diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 4b8fd74..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.github/.DS_Store b/.github/.DS_Store deleted file mode 100644 index 65d077c..0000000 Binary files a/.github/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index 2daa7b0..f8cfd1c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /croaster/build croaster-arduino/* +.DS_Store 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/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5ea5206 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,233 @@ +# 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.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) + +### 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 new file mode 100644 index 0000000..eb02eb8 --- /dev/null +++ b/FAQ.md @@ -0,0 +1,279 @@ +# ❓ 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 (WiFi) | OTA (BLE) | +|:---|:---:|:---:|:---:|:---:| +| 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 `{"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. + +--- + +### 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 +{"command": {"interval": 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 +{"command": {"tempUnit": "F"}} +``` +or +```json +{"command": {"tempUnit": "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 +{"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. + +--- + +## 🔧 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 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. + +--- + +### 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 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. + +--- + +## 🧪 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? + +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. + +--- + +### 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..86db5fc --- /dev/null +++ b/FAQ_ID.md @@ -0,0 +1,279 @@ +# ❓ 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 (WiFi) | OTA (BLE) | +|:---|:---:|:---:|:---:|:---:| +| 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 `{"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. + +--- + +### 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 +{"command": {"interval": 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 +{"command": {"tempUnit": "F"}} +``` +atau +```json +{"command": {"tempUnit": "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 +{"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. + +--- + +## 🔧 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 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. + +--- + +### 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 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. + +--- + +## 🧪 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? + +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. + +--- + +### 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..3d9a8ae 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,40 @@ # ☕ 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.50` + +--- + +## 📑 Table of Contents + +- [☕ 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) --- @@ -8,23 +42,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 (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 +* 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 +80,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**| +| |CS → **D6**|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**| +| |CS → **D8**|CS → **GPIO7**| + +> Both sensors share the **SCK** and **SO** lines (SPI bus). They are distinguished by their individual **CS** pins. + +--- + +## 🛠 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 and BLE | +| `WiFiManagerUtil` | `WiFiManagerUtil.h/.cpp` | WiFiManager captive portal setup and lifecycle | +| `DeviceIdentity` | `DeviceIdentity.h/.cpp` | Chip ID, device name, IP address helpers | + +### Data Flow + +``` +MAX6675 Sensors → CroasterCore (read + smooth + RoR) + ↓ + ┌────────────┴────────────┐ + WebSocketManager BleManager (ESP32) + ↓ ↓ + Artisan / ICRM ICRM (Android) +``` --- -## 🛠 Software Highlights +## 📦 Libraries & Dependencies -* 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.) +| 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. -1. Run the conversion script: - +### ✅ Arduino IDE (alternative, required for Makergo ESP32C3 board) + +> Arduino IDE is required if you use the **Makergo ESP32C3 SuperMini** board definition, which is not yet fully supported by PlatformIO. + +1. Run the conversion script to copy source files into the Arduino sketch folder: -```bash + ```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) -* **BLE (ESP32 only):** - + Connects exclusively with the **ICRM app** *(Android 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) --- @@ -124,21 +228,79 @@ To connect Croaster to your WiFi network, you can follow this quick video guide: 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) -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) or BLE (ESP32 only). + +- 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 + +--- + +## 🧪 Custom Commands + +Croaster accepts JSON-formatted commands over both WebSocket and BLE. The `CommandHandler` class dispatches all incoming commands. + +### 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 | +|:---|:---| +| `{"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 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. --- @@ -150,4 +312,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..8b73242 --- /dev/null +++ b/README_ID.md @@ -0,0 +1,325 @@ +# ☕ 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.50` + +--- + +## 📑 Daftar Isi + +- [☕ 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) + +--- + +## 🚀 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 (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 +* 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 → **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 → **D8**|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 dan BLE | +| `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. + +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. + +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) atau BLE (khusus ESP32). + +- 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 + +--- + +## 🧪 Perintah Kustom + +Croaster menerima perintah berformat JSON melalui WebSocket maupun BLE. Kelas `CommandHandler` mengelola semua perintah yang masuk. + +### 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 | +|:---|:---| +| `{"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 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. + +--- + +## 📘 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 diff --git a/images/Select-WebSocket-Device.png b/images/Select-WebSocket-Device.png new file mode 100644 index 0000000..58a5264 Binary files /dev/null and b/images/Select-WebSocket-Device.png differ diff --git a/platformio.ini b/platformio.ini index 28d223e..e4d1b19 100644 --- a/platformio.ini +++ b/platformio.ini @@ -11,13 +11,13 @@ [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 [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/BleManager.cpp b/src/BleManager.cpp index 3bebe1f..3e1477f 100644 --- a/src/BleManager.cpp +++ b/src/BleManager.cpp @@ -19,24 +19,70 @@ class BleManager::ServerCallbacks : public BLEServerCallbacks { debugln("# BLE Client Disconnected"); parent->clientConnected = false; + + if (parent->displayManager->isFirmwareUpdating() || 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 { + // Check OTA first to avoid parsing binary firmware bytes as a String. + if (parent->otaHandler.isReceiving()) + { + OtaResult result = parent->otaHandler.handleBinary(pCharacteristic->getData(), pCharacteristic->getLength()); + + pCharacteristic->setValue(result.json.c_str()); + pCharacteristic->notify(); + + if (result.hasError) + { + parent->displayManager->updatingStatusToggle(false); + + parent->otaHandler.finalize(true); + + return; + } + + 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, restart, erase)) + if (parent->commandHandler->handle(raw, response)) { if (!response.isEmpty()) { @@ -44,24 +90,23 @@ class BleManager::CharacteristicCallbacks : public BLECharacteristicCallbacks pCharacteristic->notify(); } - if (erase) - eraseESP(); - - if (restart) - restartESP(); - debugln("# [CMD-BLE] " + raw); + + if (!response.isEmpty()) + debugln("# [CMD-BLE-RESP] " + response); } } }; -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() { BLEDevice::init(croaster->ssidName().c_str()); + BLEDevice::setMTU(517); + pServer = BLEDevice::createServer(); pServer->setCallbacks(new ServerCallbacks(this)); @@ -75,7 +120,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(); @@ -88,6 +133,12 @@ void BleManager::begin() void BleManager::loop() { broadcastData(); + + if (otaHandler.isDone()) + { + displayManager->updatingStatusToggle(false); + otaHandler.finalize(); + } } bool BleManager::isClientConnected() const @@ -97,7 +148,7 @@ bool BleManager::isClientConnected() const void BleManager::broadcastData() { - if (!clientConnected || !pDataCharacteristic) + if (!clientConnected || !pDataCharacteristic || otaHandler.isReceiving()) return; unsigned long now = millis(); 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/CommandHandler.cpp b/src/CommandHandler.cpp index f62b652..b47b11c 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; @@ -81,19 +79,15 @@ 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") { - restart = true; - - responseOut = croaster.getJsonData(command); + ESP.restart(); } else if (command == "erase") { - erase = true; - - responseOut = croaster.getJsonData(command); + eraseESP(); } else if (command == "dummyOn") { @@ -115,6 +109,14 @@ void CommandHandler::handleBasicCommand(const JsonObject &json, String &response { displayManager.displayToggle(); } + else if (command == "getDeviceInfo") + { + responseOut = genResponseCommand(command, croaster.getDeviceInfo()); + } + else if (command == "getExtra") + { + responseOut = genResponseCommand(command, getExtraData()); + } } void CommandHandler::handleJsonCommand(const JsonObject &json, String &responseOut) @@ -175,3 +177,61 @@ 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; +} + +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 cca0238..5c1dc7d 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. @@ -47,6 +47,27 @@ 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); + + /** + * @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. @@ -69,9 +90,7 @@ 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, bool &restart, bool &erase); + bool handle(const String &json, String &responseOut); }; diff --git a/src/Constants.h b/src/Constants.h index c265011..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.45; +const double version = 0.50; // Dummy mode const bool dummyMode = false; diff --git a/src/CroasterCore.cpp b/src/CroasterCore.cpp index 6ff269a..6827373 100644 --- a/src/CroasterCore.cpp +++ b/src/CroasterCore.cpp @@ -244,63 +244,69 @@ void CroasterCore::resetHistory(String item) debugln("# Histories reset due to " + item + " change."); } -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(); + String jsonOutput; - JsonObject data = doc["data"].to(); + serializeJson(doc, jsonOutput); - if (!isnan(tempBt)) - data["BT"] = tempBt; - if (!isnan(tempEt)) - data["ET"] = tempEt; + return jsonOutput; +} - if (!skipCroaster) - { - JsonObject croaster = doc["croaster"].to(); +String CroasterCore::getDeviceInfo() +{ + String ipAddress = getIpAddress(); - croaster["interval"] = intervalSend; - croaster["timer"] = timer; - if (!isnan(tempBt)) - { - croaster["tempBt"] = tempBt; - croaster["rorBt"] = rorBt; - } - if (!isnan(tempEt)) - { - croaster["tempEt"] = tempEt; - croaster["rorEt"] = rorEt; - } + String ssid = getSsidName(); - croaster["correctionBt"] = correctionBt; - croaster["correctionEt"] = correctionEt; + JsonDocument doc; - croaster["tempUnit"] = tempUnit; - } + doc["ver"] = version; + + if (!ipAddress.isEmpty()) + doc["ip"] = ipAddress; + + 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 c752b74..59efede 100644 --- a/src/CroasterCore.h +++ b/src/CroasterCore.h @@ -58,6 +58,13 @@ class CroasterCore */ void resetHistory(String item = "something"); + /** + * @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; @@ -122,17 +129,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(); }; diff --git a/src/OtaHandler.cpp b/src/OtaHandler.cpp index 41cdc25..df10a23 100644 --- a/src/OtaHandler.cpp +++ b/src/OtaHandler.cpp @@ -22,55 +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; 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) @@ -124,6 +119,11 @@ bool OtaHandler::isReceiving() const return state == State::Receiving; } +bool OtaHandler::isDone() const +{ + return state == State::Done; +} + uint32_t OtaHandler::getTotal() const { return totalSize; diff --git a/src/OtaHandler.h b/src/OtaHandler.h index 89d9eb9..5ebd390 100644 --- a/src/OtaHandler.h +++ b/src/OtaHandler.h @@ -1,7 +1,6 @@ #pragma once #include -#include #include "Constants.h" #include "WiFiManagerUtil.h" #if defined(ESP32) @@ -10,9 +9,19 @@ #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 { @@ -32,11 +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); + OtaResult handleBinary(uint8_t *data, size_t len); /** * @brief Checks if the OTA process is currently receiving data. @@ -44,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. @@ -56,6 +69,12 @@ class OtaHandler */ uint32_t getWritten() const; + /** + * @brief Finalizes the OTA process. + * @param hasError If true, aborts the update and restarts. If false, ends successfully and restarts. + */ + void finalize(bool hasError = false); + private: /** * @enum State @@ -72,13 +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. - /** - * @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 28c5d46..1d1cd3e 100644 --- a/src/WebSocketManager.cpp +++ b/src/WebSocketManager.cpp @@ -20,25 +20,17 @@ void WebSocketManager::handleEvent(const String &cmd, uint8_t num) return; } - bool restart = false, erase = false; - String response; - if (commandHandler->handle(cmd, response, restart, erase)) + if (commandHandler->handle(cmd, response)) { if (!response.isEmpty()) - { server.sendTXT(num, response); - } - - if (erase) - eraseESP(); - - if (restart) - ESP.restart(); debugln("# [CMD-SOCKET] " + cmd); + if (!response.isEmpty()) + debugln("# [CMD-SOCKET-RESP] " + response); } } @@ -53,10 +45,12 @@ void WebSocketManager::begin() debugln("# WebSocket Client Disconnected " + String(clientConnected)); - if (displayManager->isFirmwareUpdating()) + if (displayManager->isFirmwareUpdating() || otaHandler.isReceiving()) { + debugln("# [OTA] WebSocket disconnected during OTA - restarting..."); displayManager->updatingStatusToggle(false); restartESP(); + return; } break; @@ -73,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); @@ -96,6 +101,12 @@ void WebSocketManager::loop() server.loop(); broadcastData(); + + if (otaHandler.isDone()) + { + displayManager->updatingStatusToggle(false); + otaHandler.finalize(); + } } bool WebSocketManager::isClientConnected() const @@ -106,7 +117,7 @@ bool WebSocketManager::isClientConnected() const void WebSocketManager::broadcastData() { - if (!isClientConnected()) + if (!isClientConnected() || otaHandler.isReceiving()) return; unsigned long now = millis(); 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);