#include #include #include #include #include #include #include #include #include #include #include #include #include "hx711_sensor.h" #ifndef HX711_SCK_PIN #define HX711_SCK_PIN 17 #endif #ifndef HX711_DOUT_PIN #define HX711_DOUT_PIN 18 #endif #ifndef HX711_REFERENCE_WEIGHT_GRAMS #define HX711_REFERENCE_WEIGHT_GRAMS 100.0f #endif namespace { constexpr const char *kPrefsNamespace = "filgauge"; constexpr const char *kDefaultInventreeBaseUrl = "http://192.168.0.3:1337"; constexpr const char *kDefaultInventreeUsername = "scale"; constexpr const char *kDefaultInventreePassword = "scale"; constexpr uint32_t kUiRefreshMs = 200; constexpr uint32_t kWifiPollMs = 500; constexpr uint32_t kWifiConnectTimeoutMs = 20000; constexpr uint32_t kSensorTimeoutMs = 1500; constexpr uint8_t kAverageSamples = 16; constexpr uint8_t kRawFilterWindow = 8; constexpr float kRawFilterAlpha = 0.20f; constexpr float kWeightStepHysteresisGrams = 0.75f; enum class PageId : uint8_t { Weight = 0, Calibration = 1, Wifi = 2, Inventree = 3, Count = 4 }; struct CalibrationData { int32_t offset = 0; float scale = 0.0f; bool has_offset = false; }; struct WifiData { String ssid; String password; bool connecting = false; wl_status_t last_status = WL_IDLE_STATUS; uint32_t connect_started_ms = 0; uint32_t next_poll_ms = 0; }; struct InventreeMatch { int id = 0; float quantity = 0.0f; String location; String part_name; String batch; }; constexpr size_t kInventreeMaxMatches = 8; struct InventreeData { String base_url; String token; String batch; String status_text; String result_text; int total_matches = 0; int stored_matches = 0; int selected_match = -1; InventreeMatch matches[kInventreeMaxMatches]; }; struct UiRefs { lv_obj_t *screen = nullptr; lv_obj_t *nav_panel = nullptr; lv_obj_t *content_panel = nullptr; lv_obj_t *nav_buttons[static_cast(PageId::Count)] = {}; lv_obj_t *nav_labels[static_cast(PageId::Count)] = {}; lv_obj_t *pages[static_cast(PageId::Count)] = {}; lv_obj_t *status_chip = nullptr; lv_obj_t *status_label = nullptr; lv_obj_t *weight_label = nullptr; lv_obj_t *weight_unit_label = nullptr; lv_obj_t *mode_label = nullptr; lv_obj_t *weight_note_label = nullptr; lv_obj_t *weight_raw_value = nullptr; lv_obj_t *workflow_batch_ta = nullptr; lv_obj_t *workflow_stock_name_label = nullptr; lv_obj_t *workflow_stock_meta_label = nullptr; lv_obj_t *cal_offset_value = nullptr; lv_obj_t *cal_scale_value = nullptr; lv_obj_t *cal_raw_value = nullptr; lv_obj_t *cal_note_label = nullptr; lv_obj_t *wifi_status_value = nullptr; lv_obj_t *wifi_ip_value = nullptr; lv_obj_t *wifi_ssid_ta = nullptr; lv_obj_t *wifi_password_ta = nullptr; lv_obj_t *wifi_note_label = nullptr; lv_obj_t *wifi_keyboard = nullptr; lv_obj_t *text_editor_overlay = nullptr; lv_obj_t *text_editor_panel = nullptr; lv_obj_t *text_editor_title = nullptr; lv_obj_t *text_editor_ta = nullptr; lv_obj_t *text_editor_target = nullptr; lv_obj_t *inventree_url_ta = nullptr; lv_obj_t *inventree_token_ta = nullptr; lv_obj_t *inventree_batch_ta = nullptr; lv_obj_t *inventree_status_value = nullptr; lv_obj_t *inventree_result_value = nullptr; lv_obj_t *inventree_note_label = nullptr; }; Hx711Sensor hx711; Preferences prefs; CalibrationData calibration; WifiData wifi_data; InventreeData inventree_data; UiRefs ui; PageId active_page = PageId::Weight; bool have_sample = false; bool sensor_online = false; float filtered_raw = 0.0f; int32_t last_raw = 0; int32_t raw_samples[kRawFilterWindow] = {}; uint8_t raw_sample_count = 0; uint8_t raw_sample_index = 0; int32_t displayed_weight_grams = 0; bool displayed_weight_valid = false; uint32_t last_sample_ms = 0; uint32_t next_ui_refresh_ms = 0; uint32_t last_lv_tick_ms = 0; String serial_line; constexpr size_t pageIndex(PageId page) { return static_cast(page); } void clearInventreeMatches(); void findInventreeStockByBatch(); void updateInventreeLabels(); void setLabelTextIfChanged(lv_obj_t *label, const char *text) { if (label == nullptr || text == nullptr) { return; } const char *current = lv_label_get_text(label); if (current != nullptr && strcmp(current, text) == 0) { return; } lv_label_set_text(label, text); } void setWeightStatus(const char *text, lv_color_t color) { if (ui.status_chip == nullptr || ui.status_label == nullptr) { return; } lv_obj_set_style_bg_color(ui.status_chip, color, 0); setLabelTextIfChanged(ui.status_label, text); } void setWeightNote(const char *text) { setLabelTextIfChanged(ui.weight_note_label, text); } void setCalibrationNote(const char *text) { setLabelTextIfChanged(ui.cal_note_label, text); } void setWifiNote(const char *text) { setLabelTextIfChanged(ui.wifi_note_label, text); } void setInventreeNote(const char *text) { setLabelTextIfChanged(ui.inventree_note_label, text); setLabelTextIfChanged(ui.weight_note_label, text); } void hideKeyboard() { if (ui.wifi_keyboard != nullptr) { lv_keyboard_set_textarea(ui.wifi_keyboard, nullptr); } if (ui.text_editor_ta != nullptr) { lv_textarea_set_text(ui.text_editor_ta, ""); lv_textarea_set_password_mode(ui.text_editor_ta, false); } if (ui.text_editor_overlay != nullptr) { lv_obj_add_flag(ui.text_editor_overlay, LV_OBJ_FLAG_HIDDEN); } ui.text_editor_target = nullptr; } void applyTextEditorChanges() { if (ui.text_editor_target == nullptr || ui.text_editor_ta == nullptr) { return; } lv_textarea_set_text(ui.text_editor_target, lv_textarea_get_text(ui.text_editor_ta)); if (ui.text_editor_target == ui.workflow_batch_ta || ui.text_editor_target == ui.inventree_batch_ta) { const char *batch_text = lv_textarea_get_text(ui.text_editor_target); if (ui.workflow_batch_ta != nullptr && ui.text_editor_target != ui.workflow_batch_ta) { lv_textarea_set_text(ui.workflow_batch_ta, batch_text); } if (ui.inventree_batch_ta != nullptr && ui.text_editor_target != ui.inventree_batch_ta) { lv_textarea_set_text(ui.inventree_batch_ta, batch_text); } inventree_data.batch = batch_text; inventree_data.batch.trim(); clearInventreeMatches(); if (inventree_data.batch.isEmpty()) { inventree_data.status_text = "Batch empty"; inventree_data.result_text = "Enter Batch Code to find the stock item."; setInventreeNote("Enter Batch Code, then confirm input to load the stock item."); updateInventreeLabels(); } else { findInventreeStockByBatch(); } } } void openTextEditor(lv_obj_t *target, const char *title) { if (target == nullptr || ui.text_editor_overlay == nullptr || ui.text_editor_title == nullptr || ui.text_editor_ta == nullptr || ui.wifi_keyboard == nullptr) { return; } ui.text_editor_target = target; setLabelTextIfChanged(ui.text_editor_title, title != nullptr ? title : "Edit"); lv_textarea_set_password_mode(ui.text_editor_ta, lv_textarea_get_password_mode(target)); lv_textarea_set_text(ui.text_editor_ta, lv_textarea_get_text(target)); lv_keyboard_set_textarea(ui.wifi_keyboard, ui.text_editor_ta); lv_keyboard_set_mode(ui.wifi_keyboard, LV_KEYBOARD_MODE_TEXT_LOWER); lv_obj_clear_flag(ui.text_editor_overlay, LV_OBJ_FLAG_HIDDEN); lv_obj_move_foreground(ui.text_editor_overlay); } void saveCalibration() { prefs.putLong("offset", calibration.offset); prefs.putFloat("scale", calibration.scale); prefs.putBool("has_offset", calibration.has_offset); } void loadCalibration() { calibration.offset = prefs.getLong("offset", 0); calibration.scale = prefs.getFloat("scale", 0.0f); calibration.has_offset = prefs.getBool("has_offset", false); } void saveWifiConfig() { prefs.putString("wifi_ssid", wifi_data.ssid); prefs.putString("wifi_pass", wifi_data.password); } void loadWifiConfig() { wifi_data.ssid = prefs.getString("wifi_ssid", ""); wifi_data.password = prefs.getString("wifi_pass", ""); } void saveInventreeConfig() { prefs.putString("inv_url", inventree_data.base_url); prefs.putString("inv_tok", inventree_data.token); prefs.putString("inv_batch", inventree_data.batch); } void loadInventreeConfig() { inventree_data.base_url = prefs.getString("inv_url", kDefaultInventreeBaseUrl); inventree_data.base_url.trim(); while (inventree_data.base_url.endsWith("/")) { inventree_data.base_url.remove(inventree_data.base_url.length() - 1); } if (inventree_data.base_url.isEmpty()) { inventree_data.base_url = kDefaultInventreeBaseUrl; } inventree_data.token = prefs.getString("inv_tok", ""); inventree_data.batch = prefs.getString("inv_batch", prefs.getString("inv_ipn", "")); } void setLabelLongText(lv_obj_t *label, const char *format, long value) { if (label == nullptr) { return; } char buffer[32]; snprintf(buffer, sizeof(buffer), format, value); setLabelTextIfChanged(label, buffer); } void setLabelFloatText(lv_obj_t *label, const char *format, float value) { if (label == nullptr) { return; } char buffer[64]; snprintf(buffer, sizeof(buffer), format, static_cast(value)); setLabelTextIfChanged(label, buffer); } void resetFilter(int32_t seed_value) { for (uint8_t i = 0; i < kRawFilterWindow; ++i) { raw_samples[i] = seed_value; } raw_sample_count = kRawFilterWindow; raw_sample_index = 0; filtered_raw = static_cast(seed_value); have_sample = true; } void pushRawSample(int32_t raw_value) { raw_samples[raw_sample_index] = raw_value; raw_sample_index = (raw_sample_index + 1) % kRawFilterWindow; if (raw_sample_count < kRawFilterWindow) { ++raw_sample_count; } } float getAveragedRawSample() { if (raw_sample_count == 0) { return static_cast(last_raw); } int64_t total = 0; for (uint8_t i = 0; i < raw_sample_count; ++i) { total += raw_samples[i]; } return static_cast(total) / static_cast(raw_sample_count); } int32_t applyWeightHysteresis(float grams) { if (!displayed_weight_valid) { displayed_weight_grams = static_cast(lroundf(grams)); displayed_weight_valid = true; return displayed_weight_grams; } while (grams >= static_cast(displayed_weight_grams) + kWeightStepHysteresisGrams) { ++displayed_weight_grams; } while (grams <= static_cast(displayed_weight_grams) - kWeightStepHysteresisGrams) { --displayed_weight_grams; } return displayed_weight_grams; } bool captureAverage(uint8_t samples, int32_t &value) { if (!hx711.waitReady(kSensorTimeoutMs)) { return false; } value = hx711.readAverage(samples); last_raw = value; resetFilter(value); sensor_online = true; displayed_weight_valid = false; last_sample_ms = millis(); return true; } void applyCalibrationState() { setLabelLongText(ui.cal_offset_value, "%ld", static_cast(calibration.offset)); setLabelLongText(ui.cal_raw_value, "%ld", static_cast(lroundf(filtered_raw))); if (calibration.scale > 0.0001f) { setLabelFloatText(ui.cal_scale_value, "%.2f cnt/g", calibration.scale); } else { setLabelTextIfChanged(ui.cal_scale_value, "raw mode"); } } void refreshUi() { if (ui.weight_label == nullptr) { return; } if (!sensor_online) { displayed_weight_valid = false; setLabelTextIfChanged(ui.weight_label, "--"); setLabelTextIfChanged(ui.weight_unit_label, "g"); setLabelTextIfChanged(ui.mode_label, "HX711 offline"); setLabelTextIfChanged(ui.weight_raw_value, "waiting..."); setLabelTextIfChanged(ui.cal_raw_value, "waiting..."); setWeightStatus("WAIT", lv_color_hex(0x8A6D1A)); return; } applyCalibrationState(); setLabelLongText(ui.weight_raw_value, "%ld", static_cast(lroundf(filtered_raw))); setLabelLongText(ui.cal_raw_value, "%ld", static_cast(lroundf(filtered_raw))); if (calibration.scale > 0.0001f && calibration.has_offset) { const float grams = (filtered_raw - static_cast(calibration.offset)) / calibration.scale; const int32_t stable_grams = applyWeightHysteresis(grams); setLabelLongText(ui.weight_label, "%ld", static_cast(stable_grams)); setLabelTextIfChanged(ui.weight_unit_label, "g"); setLabelTextIfChanged(ui.mode_label, "Filtered weight, ready for Batch workflow"); setWeightStatus("LIVE", lv_color_hex(0x1E7A55)); } else { displayed_weight_valid = false; setLabelLongText(ui.weight_label, "%ld", static_cast(lroundf(filtered_raw))); setLabelTextIfChanged(ui.weight_unit_label, "cnt"); setLabelTextIfChanged(ui.mode_label, "Raw mode. Open Calibration to tare and scale."); setWeightStatus("RAW", lv_color_hex(0x1F5E7A)); } } void performTare() { setWeightStatus("BUSY", lv_color_hex(0x7A5A13)); setCalibrationNote("Tare in progress..."); int32_t average = 0; if (!captureAverage(kAverageSamples, average)) { setWeightStatus("ERR", lv_color_hex(0x8B2E2E)); setCalibrationNote("HX711 did not answer during tare."); return; } calibration.offset = average; calibration.has_offset = true; saveCalibration(); displayed_weight_valid = false; refreshUi(); setWeightStatus("TARE", lv_color_hex(0x2B6E3F)); setCalibrationNote("Tare saved. Put the reference weight and tap Cal."); log_i("Tare saved, offset=%ld", static_cast(calibration.offset)); } void performCalibration(float reference_grams) { setWeightStatus("BUSY", lv_color_hex(0x7A5A13)); setCalibrationNote("Calibration in progress..."); if (reference_grams <= 0.0f) { setWeightStatus("ERR", lv_color_hex(0x8B2E2E)); setCalibrationNote("Reference weight must be greater than zero."); return; } if (!calibration.has_offset) { performTare(); if (!calibration.has_offset) { return; } } int32_t average = 0; if (!captureAverage(kAverageSamples, average)) { setWeightStatus("ERR", lv_color_hex(0x8B2E2E)); setCalibrationNote("HX711 did not answer during calibration."); return; } const float delta = static_cast(average - calibration.offset); if (fabsf(delta) < 100.0f) { setWeightStatus("ERR", lv_color_hex(0x8B2E2E)); setCalibrationNote("Signal delta is too small. Check HX711 wiring and load."); return; } calibration.scale = delta / reference_grams; saveCalibration(); displayed_weight_valid = false; refreshUi(); setWeightStatus("CAL", lv_color_hex(0x1E7A55)); setCalibrationNote("Calibration saved. Main page now shows filtered grams."); log_i("Calibration saved, scale=%f cnt/g", calibration.scale); } void clearCalibration() { calibration.scale = 0.0f; saveCalibration(); displayed_weight_valid = false; refreshUi(); setWeightStatus("RAW", lv_color_hex(0x1F5E7A)); setCalibrationNote("Scale factor cleared. Offset remains saved."); log_i("Calibration cleared"); } void setNavButtonState(PageId page, bool selected) { lv_obj_t *button = ui.nav_buttons[pageIndex(page)]; lv_obj_t *label = ui.nav_labels[pageIndex(page)]; if (button == nullptr || label == nullptr) { return; } lv_obj_set_style_bg_color(button, selected ? lv_color_hex(0x1E7A55) : lv_color_hex(0x101010), 0); lv_obj_set_style_border_color(button, selected ? lv_color_hex(0x2E9C70) : lv_color_hex(0x1C1C1C), 0); lv_obj_set_style_text_color(label, selected ? lv_color_white() : lv_color_hex(0x8FA3AC), 0); } void updateNavState() { for (size_t i = 0; i < pageIndex(PageId::Count); ++i) { setNavButtonState(static_cast(i), i == pageIndex(active_page)); } } void setActivePage(PageId page) { active_page = page; for (size_t i = 0; i < pageIndex(PageId::Count); ++i) { if (ui.pages[i] == nullptr) { continue; } if (i == pageIndex(page)) { lv_obj_clear_flag(ui.pages[i], LV_OBJ_FLAG_HIDDEN); } else { lv_obj_add_flag(ui.pages[i], LV_OBJ_FLAG_HIDDEN); } } if (page != PageId::Wifi && page != PageId::Inventree) { hideKeyboard(); } updateNavState(); } void updateWifiUi(bool force = false) { const wl_status_t status = WiFi.status(); if (!force && status == wifi_data.last_status) { return; } String status_text; if (status == WL_CONNECTED) { status_text = "Connected"; if (!WiFi.SSID().isEmpty()) { status_text += ": "; status_text += WiFi.SSID(); } } else if (wifi_data.connecting) { status_text = "Connecting"; if (!wifi_data.ssid.isEmpty()) { status_text += ": "; status_text += wifi_data.ssid; } } else if (status == WL_NO_SSID_AVAIL) { status_text = "SSID not found"; } else if (status == WL_CONNECT_FAILED) { status_text = "Password error"; } else if (status == WL_CONNECTION_LOST) { status_text = "Connection lost"; } else if (!wifi_data.ssid.isEmpty()) { status_text = "Saved: "; status_text += wifi_data.ssid; } else { status_text = "Not configured"; } String ip_text = "--"; if (status == WL_CONNECTED) { ip_text = WiFi.localIP().toString(); } setLabelTextIfChanged(ui.wifi_status_value, status_text.c_str()); setLabelTextIfChanged(ui.wifi_ip_value, ip_text.c_str()); wifi_data.last_status = status; } void connectWifi(const String &ssid, const String &password, bool save_credentials) { String trimmed_ssid = ssid; trimmed_ssid.trim(); if (trimmed_ssid.isEmpty()) { setWifiNote("Enter Wi-Fi SSID first."); return; } wifi_data.ssid = trimmed_ssid; wifi_data.password = password; if (save_credentials) { saveWifiConfig(); } hideKeyboard(); WiFi.persistent(false); WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true); WiFi.begin(wifi_data.ssid.c_str(), wifi_data.password.c_str()); wifi_data.connecting = true; wifi_data.connect_started_ms = millis(); wifi_data.last_status = WL_IDLE_STATUS; wifi_data.next_poll_ms = 0; setWifiNote("Connecting to Wi-Fi..."); updateWifiUi(true); } void connectWifiFromForm(bool save_credentials) { String ssid = ui.wifi_ssid_ta != nullptr ? String(lv_textarea_get_text(ui.wifi_ssid_ta)) : String(); String password = ui.wifi_password_ta != nullptr ? String(lv_textarea_get_text(ui.wifi_password_ta)) : String(); connectWifi(ssid, password, save_credentials); } void disconnectWifi() { hideKeyboard(); wifi_data.connecting = false; WiFi.disconnect(true, false); WiFi.mode(WIFI_OFF); setWifiNote("Wi-Fi disconnected."); updateWifiUi(true); } void handleWifi() { const uint32_t now = millis(); if (now < wifi_data.next_poll_ms) { return; } wifi_data.next_poll_ms = now + kWifiPollMs; const wl_status_t status = WiFi.status(); if (status == WL_CONNECTED) { if (wifi_data.connecting) { wifi_data.connecting = false; setWifiNote("Wi-Fi connected."); } } else if (wifi_data.connecting && now - wifi_data.connect_started_ms > kWifiConnectTimeoutMs) { wifi_data.connecting = false; WiFi.disconnect(true, false); WiFi.mode(WIFI_OFF); setWifiNote("Connection timeout. Check SSID and password."); } else if (status != wifi_data.last_status) { if (status == WL_CONNECT_FAILED) { wifi_data.connecting = false; setWifiNote("Connection failed. Check password."); } else if (status == WL_NO_SSID_AVAIL) { wifi_data.connecting = false; setWifiNote("SSID not found."); } else if (status == WL_CONNECTION_LOST) { wifi_data.connecting = false; setWifiNote("Wi-Fi connection lost."); } else if (status == WL_DISCONNECTED && !wifi_data.connecting && !wifi_data.ssid.isEmpty()) { setWifiNote("Wi-Fi disconnected."); } } updateWifiUi(true); } String normalizeInventreeBaseUrl(const String &value) { String base_url = value; base_url.trim(); while (base_url.endsWith("/")) { base_url.remove(base_url.length() - 1); } return base_url; } String urlEncode(const String &value) { String encoded; encoded.reserve(value.length() * 3U); for (size_t i = 0; i < value.length(); ++i) { const uint8_t ch = static_cast(value[i]); if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_' || ch == '.' || ch == '~') { encoded += static_cast(ch); } else { char buffer[4]; snprintf(buffer, sizeof(buffer), "%%%02X", ch); encoded += buffer; } } return encoded; } void syncInventreeInputs(bool persist) { inventree_data.base_url = ui.inventree_url_ta != nullptr ? normalizeInventreeBaseUrl(String(lv_textarea_get_text(ui.inventree_url_ta))) : inventree_data.base_url; inventree_data.token = ui.inventree_token_ta != nullptr ? String(lv_textarea_get_text(ui.inventree_token_ta)) : inventree_data.token; inventree_data.token.trim(); lv_obj_t *batch_input = nullptr; if (active_page == PageId::Weight && ui.workflow_batch_ta != nullptr) { batch_input = ui.workflow_batch_ta; } else if (ui.inventree_batch_ta != nullptr) { batch_input = ui.inventree_batch_ta; } else { batch_input = ui.workflow_batch_ta; } inventree_data.batch = batch_input != nullptr ? String(lv_textarea_get_text(batch_input)) : inventree_data.batch; inventree_data.batch.trim(); if (ui.workflow_batch_ta != nullptr && batch_input != ui.workflow_batch_ta) { lv_textarea_set_text(ui.workflow_batch_ta, inventree_data.batch.c_str()); } if (ui.inventree_batch_ta != nullptr && batch_input != ui.inventree_batch_ta) { lv_textarea_set_text(ui.inventree_batch_ta, inventree_data.batch.c_str()); } if (persist) { saveInventreeConfig(); } } bool inventreeHasBasicCredentials() { return kDefaultInventreeUsername[0] != '\0' && kDefaultInventreePassword[0] != '\0'; } bool inventreeConfigured() { return !inventree_data.base_url.isEmpty() && (!inventree_data.token.isEmpty() || inventreeHasBasicCredentials()); } void clearInventreeMatches() { inventree_data.total_matches = 0; inventree_data.stored_matches = 0; inventree_data.selected_match = -1; } void updateInventreeLabels() { setLabelTextIfChanged(ui.inventree_status_value, inventree_data.status_text.isEmpty() ? "Not configured" : inventree_data.status_text.c_str()); setLabelTextIfChanged(ui.inventree_result_value, inventree_data.result_text.isEmpty() ? "No stock item selected." : inventree_data.result_text.c_str()); if (ui.workflow_stock_name_label != nullptr && ui.workflow_stock_meta_label != nullptr) { if (inventree_data.selected_match >= 0 && inventree_data.selected_match < inventree_data.stored_matches) { const InventreeMatch &match = inventree_data.matches[inventree_data.selected_match]; String title = match.part_name.isEmpty() ? "Stock item ID " + String(match.id) : match.part_name; String meta; meta.reserve(192); meta += "Batch "; meta += match.batch.isEmpty() ? "--" : match.batch; meta += " | Qty "; meta += String(match.quantity, 1); if (!match.location.isEmpty()) { meta += " | "; meta += match.location; } setLabelTextIfChanged(ui.workflow_stock_name_label, title.c_str()); setLabelTextIfChanged(ui.workflow_stock_meta_label, meta.c_str()); } else if (!inventree_data.batch.isEmpty()) { setLabelTextIfChanged(ui.workflow_stock_name_label, "Stock item not selected"); setLabelTextIfChanged(ui.workflow_stock_meta_label, inventree_data.result_text.isEmpty() ? "No matches for the entered Batch Code." : inventree_data.result_text.c_str()); } else { setLabelTextIfChanged(ui.workflow_stock_name_label, "Enter Batch Code"); setLabelTextIfChanged(ui.workflow_stock_meta_label, "The device will load the stock item and prepare the current weight for writeback."); } } } void updateInventreeSelectionSummary() { if (inventree_data.selected_match < 0 || inventree_data.selected_match >= inventree_data.stored_matches) { inventree_data.result_text = "No stock item selected."; updateInventreeLabels(); return; } const InventreeMatch &match = inventree_data.matches[inventree_data.selected_match]; String text = "Match "; text += String(inventree_data.selected_match + 1); text += "/"; text += String(inventree_data.total_matches); text += " ID "; text += String(match.id); text += " Qty "; text += String(match.quantity, 1); if (!match.location.isEmpty()) { text += "\nLoc: "; text += match.location; } if (!match.batch.isEmpty() || !match.part_name.isEmpty()) { text += "\n"; if (!match.batch.isEmpty()) { text += "Batch "; text += match.batch; } if (!match.part_name.isEmpty()) { if (!match.batch.isEmpty()) { text += " | "; } text += match.part_name; } } inventree_data.result_text = text; updateInventreeLabels(); } String buildInventreeUrl(const String &path) { if (path.startsWith("http://") || path.startsWith("https://")) { return path; } if (path.startsWith("/")) { return inventree_data.base_url + path; } return inventree_data.base_url + "/" + path; } template bool performInventreeHttpRequest(TClient &client, const String &url, const char *method, const String &request_body, bool prefer_token_auth, int &http_code, String &response_body, String &error_text) { HTTPClient http; http.setTimeout(12000); if (!http.begin(client, url)) { error_text = "HTTP begin failed."; return false; } if (prefer_token_auth && !inventree_data.token.isEmpty()) { http.addHeader("Authorization", "Token " + inventree_data.token); } else if (inventreeHasBasicCredentials()) { http.setAuthorization(kDefaultInventreeUsername, kDefaultInventreePassword); } else { http.end(); error_text = "Enter InvenTree API token."; return false; } http.addHeader("Accept", "application/json"); if (!request_body.isEmpty()) { http.addHeader("Content-Type", "application/json"); } if (strcmp(method, "GET") == 0) { http_code = http.GET(); } else if (strcmp(method, "POST") == 0) { http_code = http.POST(request_body); } else { http_code = http.sendRequest(method, request_body); } if (http_code > 0) { response_body = http.getString(); } else { error_text = HTTPClient::errorToString(http_code); } http.end(); return http_code > 0; } bool requestInventreeToken(String &error_text) { error_text = ""; if (WiFi.status() != WL_CONNECTED) { error_text = "Wi-Fi is not connected."; return false; } if (inventree_data.base_url.isEmpty()) { error_text = "Enter InvenTree URL."; return false; } if (!inventreeHasBasicCredentials()) { error_text = "No InvenTree login credentials configured."; return false; } int http_code = 0; String response_body; const String url = buildInventreeUrl("/api/user/token/?name=" + urlEncode("FilamentGauger")); const bool is_https = url.startsWith("https://"); bool request_ok = false; if (is_https) { WiFiClientSecure client; client.setInsecure(); request_ok = performInventreeHttpRequest(client, url, "GET", "", false, http_code, response_body, error_text); } else { WiFiClient client; request_ok = performInventreeHttpRequest(client, url, "GET", "", false, http_code, response_body, error_text); } if (!request_ok) { return false; } if (http_code != 200) { error_text = response_body.length() > 0 ? response_body : "Unexpected token response."; return false; } JsonDocument token_doc; const DeserializationError json_error = deserializeJson(token_doc, response_body); if (json_error) { error_text = json_error.c_str(); return false; } inventree_data.token = String(token_doc["token"] | ""); inventree_data.token.trim(); if (inventree_data.token.isEmpty()) { error_text = "InvenTree returned an empty token."; return false; } if (ui.inventree_token_ta != nullptr) { lv_textarea_set_text(ui.inventree_token_ta, inventree_data.token.c_str()); } saveInventreeConfig(); return true; } bool performInventreeRequest(const char *method, const String &path, const String &request_body, int &http_code, String &response_body, String &error_text) { response_body = ""; error_text = ""; http_code = 0; if (WiFi.status() != WL_CONNECTED) { error_text = "Wi-Fi is not connected."; return false; } if (!inventreeConfigured()) { error_text = "Enter InvenTree URL."; return false; } const String url = buildInventreeUrl(path); if (inventree_data.token.isEmpty() && inventreeHasBasicCredentials()) { String token_error; if (!requestInventreeToken(token_error)) { Serial.printf("InvenTree token fetch failed, using basic auth: %s\n", token_error.c_str()); } } const bool prefer_token_auth = !inventree_data.token.isEmpty(); const bool is_https = url.startsWith("https://"); if (is_https) { WiFiClientSecure client; client.setInsecure(); const bool request_ok = performInventreeHttpRequest(client, url, method, request_body, prefer_token_auth, http_code, response_body, error_text); if (request_ok && http_code == 401 && prefer_token_auth && inventreeHasBasicCredentials()) { inventree_data.token = ""; if (ui.inventree_token_ta != nullptr) { lv_textarea_set_text(ui.inventree_token_ta, ""); } saveInventreeConfig(); return performInventreeHttpRequest(client, url, method, request_body, false, http_code, response_body, error_text); } return request_ok; } WiFiClient client; const bool request_ok = performInventreeHttpRequest(client, url, method, request_body, prefer_token_auth, http_code, response_body, error_text); if (request_ok && http_code == 401 && prefer_token_auth && inventreeHasBasicCredentials()) { inventree_data.token = ""; if (ui.inventree_token_ta != nullptr) { lv_textarea_set_text(ui.inventree_token_ta, ""); } saveInventreeConfig(); return performInventreeHttpRequest(client, url, method, request_body, false, http_code, response_body, error_text); } return request_ok; } bool getMeasuredWeightForInventree(String &quantity_text) { quantity_text = ""; if (!sensor_online || !calibration.has_offset || calibration.scale <= 0.0001f) { return false; } const float grams = (filtered_raw - static_cast(calibration.offset)) / calibration.scale; const int32_t stable_grams = displayed_weight_valid ? displayed_weight_grams : static_cast(lroundf(grams)); quantity_text = String(stable_grams < 0 ? 0 : stable_grams); return true; } void saveInventreeSettingsFromForm() { syncInventreeInputs(true); clearInventreeMatches(); if (!inventreeConfigured()) { inventree_data.status_text = "Missing URL"; inventree_data.result_text = "Fill Base URL, then save."; setInventreeNote("InvenTree settings were not saved because Base URL is empty."); } else { inventree_data.status_text = "Settings saved"; inventree_data.result_text = inventree_data.token.isEmpty() ? "Saved URL and Batch Code. Token will be requested automatically from login." : "Saved URL, token and Batch Code to device storage."; setInventreeNote("Saved InvenTree settings. API token is optional on this device."); } updateInventreeLabels(); } void testInventreeApi() { syncInventreeInputs(false); clearInventreeMatches(); int http_code = 0; String response; String error_text; if (!performInventreeRequest("GET", "/api/stock/?limit=1", "", http_code, response, error_text)) { inventree_data.status_text = "API error"; inventree_data.result_text = error_text; setInventreeNote("Could not reach InvenTree API."); updateInventreeLabels(); return; } if (http_code != 200) { inventree_data.status_text = "HTTP " + String(http_code); inventree_data.result_text = response.length() > 0 ? response : "Unexpected API response."; setInventreeNote("API test failed."); updateInventreeLabels(); return; } inventree_data.status_text = "API OK"; inventree_data.result_text = "Connected to " + inventree_data.base_url; setInventreeNote("InvenTree API authentication succeeded."); updateInventreeLabels(); } void findInventreeStockByBatch() { syncInventreeInputs(false); clearInventreeMatches(); if (inventree_data.batch.isEmpty()) { inventree_data.status_text = "Missing Batch"; inventree_data.result_text = "Enter the Batch Code to search."; setInventreeNote("Batch Code is required for search."); updateInventreeLabels(); return; } const String path = "/api/stock/?batch=" + urlEncode(inventree_data.batch) + "&limit=" + String(kInventreeMaxMatches) + "&part_detail=true&location_detail=true"; int http_code = 0; String response; String error_text; if (!performInventreeRequest("GET", path, "", http_code, response, error_text)) { inventree_data.status_text = "Search error"; inventree_data.result_text = error_text; setInventreeNote("Could not search InvenTree stock."); updateInventreeLabels(); return; } if (http_code != 200) { inventree_data.status_text = "HTTP " + String(http_code); inventree_data.result_text = response.length() > 0 ? response : "Unexpected API response."; setInventreeNote("Search request failed."); updateInventreeLabels(); return; } JsonDocument filter; filter["count"] = true; JsonArray filter_results = filter["results"].to(); JsonObject filter_item = filter_results.add(); filter_item["pk"] = true; filter_item["batch"] = true; filter_item["quantity"] = true; filter_item["location"] = true; filter_item["location_detail"]["pathstring"] = true; filter_item["location_detail"]["name"] = true; filter_item["part_detail"]["name"] = true; JsonDocument doc; const DeserializationError json_error = deserializeJson(doc, response, DeserializationOption::Filter(filter)); if (json_error) { inventree_data.status_text = "JSON error"; inventree_data.result_text = json_error.c_str(); setInventreeNote("Could not parse InvenTree response."); updateInventreeLabels(); return; } const JsonArray results = doc["results"].as(); inventree_data.total_matches = doc["count"] | static_cast(results.size()); inventree_data.stored_matches = 0; for (JsonObject item : results) { if (inventree_data.stored_matches >= static_cast(kInventreeMaxMatches)) { break; } InventreeMatch &match = inventree_data.matches[inventree_data.stored_matches]; match.id = item["pk"] | 0; match.quantity = item["quantity"] | 0.0f; match.batch = String(static_cast(item["batch"] | "")); match.part_name = String(static_cast(item["part_detail"]["name"] | "")); const char *pathstring = item["location_detail"]["pathstring"] | nullptr; const char *name = item["location_detail"]["name"] | nullptr; if (pathstring != nullptr && strlen(pathstring) > 0U) { match.location = pathstring; } else if (name != nullptr && strlen(name) > 0U) { match.location = name; } else { match.location = "Location #" + String(item["location"] | 0); } ++inventree_data.stored_matches; } if (inventree_data.stored_matches == 0) { inventree_data.status_text = "No matches"; inventree_data.result_text = "No stock items found for Batch " + inventree_data.batch; setInventreeNote("Search completed but no stock item matched that Batch Code."); updateInventreeLabels(); return; } inventree_data.selected_match = 0; inventree_data.status_text = "Found " + String(inventree_data.total_matches) + " item(s)"; setInventreeNote(inventree_data.total_matches > inventree_data.stored_matches ? "More items exist than shown here. Use the first loaded results." : "Use Prev / Next if multiple stock items were found."); updateInventreeSelectionSummary(); } void selectInventreeMatch(int delta) { if (inventree_data.stored_matches <= 0) { setInventreeNote("Search stock items first."); return; } inventree_data.selected_match += delta; if (inventree_data.selected_match < 0) { inventree_data.selected_match = inventree_data.stored_matches - 1; } else if (inventree_data.selected_match >= inventree_data.stored_matches) { inventree_data.selected_match = 0; } updateInventreeSelectionSummary(); } void pushMeasuredWeightToInventree() { syncInventreeInputs(false); if (inventree_data.selected_match < 0 || inventree_data.selected_match >= inventree_data.stored_matches) { setInventreeNote("No stock item selected. Run Find first."); return; } String quantity_text; if (!getMeasuredWeightForInventree(quantity_text)) { setInventreeNote("Weight is not ready. Calibrate HX711 in grams first."); return; } JsonDocument body_doc; JsonArray items = body_doc["items"].to(); JsonObject item = items.add(); item["pk"] = inventree_data.matches[inventree_data.selected_match].id; item["quantity"] = quantity_text; body_doc["notes"] = "Updated from FilamentGauger sensor"; String request_body; serializeJson(body_doc, request_body); int http_code = 0; String response; String error_text; if (!performInventreeRequest("POST", "/api/stock/count/", request_body, http_code, response, error_text)) { inventree_data.status_text = "Update error"; inventree_data.result_text = error_text; setInventreeNote("Could not submit measured weight to InvenTree."); updateInventreeLabels(); return; } if (http_code != 200 && http_code != 201) { inventree_data.status_text = "HTTP " + String(http_code); inventree_data.result_text = response.length() > 0 ? response : "Unexpected API response."; setInventreeNote("Stock count update failed."); updateInventreeLabels(); return; } inventree_data.status_text = "Stock updated"; inventree_data.result_text = "Sent " + quantity_text + " g to stock item ID " + String(inventree_data.matches[inventree_data.selected_match].id); setInventreeNote("Measured weight was written to InvenTree stock quantity."); updateInventreeLabels(); findInventreeStockByBatch(); setInventreeNote("Measured weight was written to InvenTree stock quantity."); } lv_obj_t *createPageContainer(lv_obj_t *parent) { lv_obj_t *page = lv_obj_create(parent); lv_obj_set_size(page, 608, 456); lv_obj_set_pos(page, 0, 0); lv_obj_remove_flag(page, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_bg_color(page, lv_color_black(), 0); lv_obj_set_style_bg_grad_color(page, lv_color_black(), 0); lv_obj_set_style_border_width(page, 0, 0); lv_obj_set_style_radius(page, 24, 0); lv_obj_set_style_pad_all(page, 0, 0); return page; } lv_obj_t *createMetricCard(lv_obj_t *parent, const char *title, lv_coord_t x, lv_coord_t y, lv_coord_t width, lv_coord_t height, const lv_font_t *value_font, lv_obj_t **value_label) { lv_obj_t *card = lv_obj_create(parent); lv_obj_set_size(card, width, height); lv_obj_set_pos(card, x, y); lv_obj_remove_flag(card, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_radius(card, 18, 0); lv_obj_set_style_bg_color(card, lv_color_black(), 0); lv_obj_set_style_bg_grad_color(card, lv_color_black(), 0); lv_obj_set_style_border_color(card, lv_color_hex(0x1A1A1A), 0); lv_obj_set_style_border_width(card, 1, 0); lv_obj_set_style_pad_all(card, 16, 0); lv_obj_t *title_label = lv_label_create(card); lv_obj_set_style_text_color(title_label, lv_color_hex(0x6F8893), 0); lv_obj_set_style_text_font(title_label, &lv_font_montserrat_14, 0); lv_label_set_text(title_label, title); lv_obj_align(title_label, LV_ALIGN_TOP_LEFT, 0, 0); *value_label = lv_label_create(card); lv_obj_set_width(*value_label, width - 32); lv_obj_set_style_text_color(*value_label, lv_color_hex(0xF5FAFC), 0); lv_obj_set_style_text_font(*value_label, value_font, 0); lv_label_set_long_mode(*value_label, LV_LABEL_LONG_WRAP); lv_label_set_text(*value_label, "--"); lv_obj_align(*value_label, LV_ALIGN_BOTTOM_LEFT, 0, 0); return card; } lv_obj_t *createActionButton(lv_obj_t *parent, const char *label_text, lv_coord_t x, lv_coord_t y, lv_coord_t width, lv_coord_t height, lv_color_t color, lv_event_cb_t cb) { lv_obj_t *button = lv_button_create(parent); lv_obj_set_size(button, width, height); lv_obj_set_pos(button, x, y); lv_obj_remove_flag(button, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_radius(button, 18, 0); lv_obj_set_style_bg_color(button, color, 0); lv_obj_set_style_border_width(button, 0, 0); lv_obj_set_style_shadow_width(button, 0, 0); lv_obj_set_style_shadow_opa(button, LV_OPA_TRANSP, 0); lv_obj_add_event_cb(button, cb, LV_EVENT_CLICKED, nullptr); lv_obj_t *label = lv_label_create(button); lv_obj_set_style_text_font(label, &lv_font_montserrat_18, 0); lv_obj_set_style_text_color(label, lv_color_white(), 0); lv_label_set_text(label, label_text); lv_obj_center(label); return button; } lv_obj_t *createNavButton(lv_obj_t *parent, const char *label_text, lv_coord_t y, lv_event_cb_t cb, PageId page) { constexpr lv_coord_t kButtonWidth = 116; lv_obj_t *button = lv_button_create(parent); lv_obj_set_size(button, kButtonWidth, 58); lv_obj_set_pos(button, (156 - kButtonWidth) / 2, y); lv_obj_remove_flag(button, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_radius(button, 18, 0); lv_obj_set_style_bg_color(button, lv_color_hex(0x101010), 0); lv_obj_set_style_border_color(button, lv_color_hex(0x1C1C1C), 0); lv_obj_set_style_border_width(button, 1, 0); lv_obj_set_style_shadow_width(button, 0, 0); lv_obj_add_event_cb(button, cb, LV_EVENT_CLICKED, nullptr); lv_obj_t *label = lv_label_create(button); lv_obj_set_style_text_font(label, &lv_font_montserrat_18, 0); lv_obj_set_style_text_color(label, lv_color_hex(0x8FA3AC), 0); lv_label_set_text(label, label_text); lv_obj_center(label); ui.nav_buttons[pageIndex(page)] = button; ui.nav_labels[pageIndex(page)] = label; return button; } void tareButtonEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { performTare(); } } void calibrateButtonEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { performCalibration(HX711_REFERENCE_WEIGHT_GRAMS); } } void resetButtonEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { clearCalibration(); } } void navWeightEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { setActivePage(PageId::Weight); } } void navCalibrationEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { setActivePage(PageId::Calibration); } } void navWifiEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { setActivePage(PageId::Wifi); } } void navInventreeEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { setActivePage(PageId::Inventree); } } void wifiConnectButtonEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { connectWifiFromForm(false); } } void wifiSaveConnectButtonEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { connectWifiFromForm(true); } } void wifiDisconnectButtonEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { disconnectWifi(); } } void inventreeSaveButtonEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { saveInventreeSettingsFromForm(); } } void inventreeTestButtonEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { testInventreeApi(); } } void inventreeFindButtonEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { findInventreeStockByBatch(); } } void inventreeNextButtonEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { selectInventreeMatch(1); } } void inventreePushButtonEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { pushMeasuredWeightToInventree(); } } void textEditorApplyButtonEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { applyTextEditorChanges(); hideKeyboard(); } } void textEditorCancelButtonEvent(lv_event_t *event) { if (lv_event_get_code(event) == LV_EVENT_CLICKED) { hideKeyboard(); } } void textInputEvent(lv_event_t *event) { if (lv_event_get_code(event) != LV_EVENT_CLICKED) { return; } lv_obj_t *target = static_cast(lv_event_get_target(event)); const char *title = static_cast(lv_event_get_user_data(event)); openTextEditor(target, title); } void textEditorKeyboardEvent(lv_event_t *event) { const lv_event_code_t code = lv_event_get_code(event); if (code == LV_EVENT_READY) { applyTextEditorChanges(); hideKeyboard(); } else if (code == LV_EVENT_CANCEL) { hideKeyboard(); } } lv_obj_t *createTextInput(lv_obj_t *parent, lv_coord_t x, lv_coord_t y, lv_coord_t width, const char *editor_title, const char *placeholder, bool password_mode) { lv_obj_t *textarea = lv_textarea_create(parent); lv_obj_set_size(textarea, width, 46); lv_obj_set_pos(textarea, x, y); lv_obj_set_style_radius(textarea, 14, 0); lv_obj_set_style_bg_color(textarea, lv_color_black(), 0); lv_obj_set_style_border_color(textarea, lv_color_hex(0x1A1A1A), 0); lv_obj_set_style_border_width(textarea, 1, 0); lv_obj_set_style_text_color(textarea, lv_color_hex(0xF5FAFC), 0); lv_obj_set_style_pad_hor(textarea, 14, 0); lv_obj_set_style_pad_ver(textarea, 10, 0); lv_textarea_set_one_line(textarea, true); lv_textarea_set_password_mode(textarea, password_mode); lv_textarea_set_placeholder_text(textarea, placeholder); lv_obj_add_event_cb(textarea, textInputEvent, LV_EVENT_CLICKED, const_cast(editor_title)); return textarea; } void buildWeightPage() { lv_obj_t *page = createPageContainer(ui.content_panel); ui.pages[pageIndex(PageId::Weight)] = page; lv_obj_t *title = lv_label_create(page); lv_obj_set_pos(title, 24, 18); lv_obj_set_style_text_color(title, lv_color_hex(0xF5FAFC), 0); lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0); lv_label_set_text(title, "Main"); lv_obj_t *subtitle = lv_label_create(page); lv_obj_set_pos(subtitle, 24, 48); lv_obj_set_style_text_color(subtitle, lv_color_hex(0x6F8893), 0); lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_14, 0); lv_label_set_text(subtitle, "Weigh filament spools by Batch Code and write grams to InvenTree"); ui.status_chip = lv_obj_create(page); lv_obj_set_size(ui.status_chip, 110, 40); lv_obj_set_pos(ui.status_chip, 474, 20); lv_obj_remove_flag(ui.status_chip, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_radius(ui.status_chip, 20, 0); lv_obj_set_style_border_width(ui.status_chip, 0, 0); lv_obj_set_style_pad_all(ui.status_chip, 0, 0); lv_obj_set_style_bg_color(ui.status_chip, lv_color_hex(0x8A6D1A), 0); ui.status_label = lv_label_create(ui.status_chip); lv_obj_set_style_text_font(ui.status_label, &lv_font_montserrat_18, 0); lv_obj_set_style_text_color(ui.status_label, lv_color_white(), 0); lv_label_set_text(ui.status_label, "WAIT"); lv_obj_center(ui.status_label); ui.weight_label = lv_label_create(page); lv_obj_set_pos(ui.weight_label, 18, 92); lv_obj_set_width(ui.weight_label, 470); lv_obj_set_style_text_color(ui.weight_label, lv_color_hex(0xF8FEFF), 0); lv_obj_set_style_text_font(ui.weight_label, &lv_font_montserrat_48, 0); lv_obj_set_style_text_align(ui.weight_label, LV_TEXT_ALIGN_CENTER, 0); lv_label_set_text(ui.weight_label, "--"); ui.weight_unit_label = lv_label_create(page); lv_obj_set_pos(ui.weight_unit_label, 500, 130); lv_obj_set_style_text_color(ui.weight_unit_label, lv_color_hex(0x8FA3AC), 0); lv_obj_set_style_text_font(ui.weight_unit_label, &lv_font_montserrat_24, 0); lv_label_set_text(ui.weight_unit_label, "g"); ui.mode_label = lv_label_create(page); lv_obj_set_pos(ui.mode_label, 24, 166); lv_obj_set_width(ui.mode_label, 550); lv_obj_set_style_text_color(ui.mode_label, lv_color_hex(0x7FA1B2), 0); lv_obj_set_style_text_font(ui.mode_label, &lv_font_montserrat_18, 0); lv_obj_set_style_text_align(ui.mode_label, LV_TEXT_ALIGN_CENTER, 0); lv_label_set_long_mode(ui.mode_label, LV_LABEL_LONG_WRAP); lv_label_set_text(ui.mode_label, "Waiting for HX711..."); ui.weight_raw_value = nullptr; lv_obj_t *batch_label = lv_label_create(page); lv_obj_set_pos(batch_label, 24, 212); lv_obj_set_style_text_color(batch_label, lv_color_hex(0x7FA1B2), 0); lv_obj_set_style_text_font(batch_label, &lv_font_montserrat_14, 0); lv_label_set_text(batch_label, "Batch Code"); ui.workflow_batch_ta = createTextInput(page, 24, 236, 560, "Main Batch Code", "Scan or type Batch Code", false); if (!inventree_data.batch.isEmpty()) { lv_textarea_set_text(ui.workflow_batch_ta, inventree_data.batch.c_str()); } lv_obj_t *stock_title = lv_label_create(page); lv_obj_set_pos(stock_title, 24, 296); lv_obj_set_style_text_color(stock_title, lv_color_hex(0x7FA1B2), 0); lv_obj_set_style_text_font(stock_title, &lv_font_montserrat_14, 0); lv_label_set_text(stock_title, "Stock Item"); ui.workflow_stock_name_label = lv_label_create(page); lv_obj_set_pos(ui.workflow_stock_name_label, 24, 320); lv_obj_set_width(ui.workflow_stock_name_label, 560); lv_obj_set_style_text_color(ui.workflow_stock_name_label, lv_color_hex(0xF5FAFC), 0); lv_obj_set_style_text_font(ui.workflow_stock_name_label, &lv_font_montserrat_20, 0); lv_label_set_long_mode(ui.workflow_stock_name_label, LV_LABEL_LONG_WRAP); lv_label_set_text(ui.workflow_stock_name_label, "Enter Batch Code"); ui.workflow_stock_meta_label = lv_label_create(page); lv_obj_set_pos(ui.workflow_stock_meta_label, 24, 352); lv_obj_set_width(ui.workflow_stock_meta_label, 560); lv_obj_set_style_text_color(ui.workflow_stock_meta_label, lv_color_hex(0x8FA3AC), 0); lv_obj_set_style_text_font(ui.workflow_stock_meta_label, &lv_font_montserrat_14, 0); lv_label_set_long_mode(ui.workflow_stock_meta_label, LV_LABEL_LONG_WRAP); lv_label_set_text(ui.workflow_stock_meta_label, "The device will load the stock item and prepare the current weight for writeback."); createActionButton(page, "Write Weight", 24, 392, 560, 44, lv_color_hex(0x7A3A2A), inventreePushButtonEvent); ui.weight_note_label = lv_label_create(page); lv_obj_set_pos(ui.weight_note_label, 24, 438); lv_obj_set_width(ui.weight_note_label, 560); lv_obj_set_style_text_color(ui.weight_note_label, lv_color_hex(0x8FA3AC), 0); lv_obj_set_style_text_font(ui.weight_note_label, &lv_font_montserrat_14, 0); lv_obj_set_style_text_align(ui.weight_note_label, LV_TEXT_ALIGN_CENTER, 0); lv_label_set_long_mode(ui.weight_note_label, LV_LABEL_LONG_WRAP); lv_label_set_text(ui.weight_note_label, "Enter Batch Code, confirm input, then tap Write Weight."); } void buildCalibrationPage() { lv_obj_t *page = createPageContainer(ui.content_panel); ui.pages[pageIndex(PageId::Calibration)] = page; lv_obj_t *title = lv_label_create(page); lv_obj_set_pos(title, 24, 18); lv_obj_set_style_text_color(title, lv_color_hex(0xF5FAFC), 0); lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0); lv_label_set_text(title, "Calibration"); lv_obj_t *subtitle = lv_label_create(page); lv_obj_set_pos(subtitle, 24, 48); lv_obj_set_style_text_color(subtitle, lv_color_hex(0x6F8893), 0); lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_14, 0); lv_label_set_text(subtitle, "Tare the empty platform, then calibrate with the reference weight"); createMetricCard(page, "RAW", 24, 98, 170, 104, &lv_font_montserrat_24, &ui.cal_raw_value); createMetricCard(page, "OFFSET", 214, 98, 170, 104, &lv_font_montserrat_24, &ui.cal_offset_value); createMetricCard(page, "SCALE", 404, 98, 170, 104, &lv_font_montserrat_24, &ui.cal_scale_value); createActionButton(page, "Tare", 24, 238, 170, 62, lv_color_hex(0x245C7A), tareButtonEvent); char calibrate_text[32]; snprintf(calibrate_text, sizeof(calibrate_text), "Cal %.0f g", static_cast(HX711_REFERENCE_WEIGHT_GRAMS)); createActionButton(page, calibrate_text, 214, 238, 170, 62, lv_color_hex(0x28714A), calibrateButtonEvent); createActionButton(page, "Reset Cal", 404, 238, 170, 62, lv_color_hex(0x7A3A2A), resetButtonEvent); ui.cal_note_label = lv_label_create(page); lv_obj_set_pos(ui.cal_note_label, 24, 332); lv_obj_set_width(ui.cal_note_label, 550); lv_obj_set_style_text_color(ui.cal_note_label, lv_color_hex(0x8FA3AC), 0); lv_obj_set_style_text_font(ui.cal_note_label, &lv_font_montserrat_14, 0); lv_label_set_long_mode(ui.cal_note_label, LV_LABEL_LONG_WRAP); lv_label_set_text(ui.cal_note_label, "Empty the platform, tap Tare, place the 100 g reference weight, then tap Cal."); } void buildWifiPage() { lv_obj_t *page = createPageContainer(ui.content_panel); ui.pages[pageIndex(PageId::Wifi)] = page; lv_obj_t *title = lv_label_create(page); lv_obj_set_pos(title, 24, 18); lv_obj_set_style_text_color(title, lv_color_hex(0xF5FAFC), 0); lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0); lv_label_set_text(title, "Wi-Fi"); lv_obj_t *subtitle = lv_label_create(page); lv_obj_set_pos(subtitle, 24, 48); lv_obj_set_style_text_color(subtitle, lv_color_hex(0x6F8893), 0); lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_14, 0); lv_label_set_text(subtitle, "Save credentials and connect without leaving the device"); lv_obj_t *status_label = lv_label_create(page); lv_obj_set_pos(status_label, 24, 98); lv_obj_set_style_text_color(status_label, lv_color_hex(0x7FA1B2), 0); lv_obj_set_style_text_font(status_label, &lv_font_montserrat_14, 0); lv_label_set_text(status_label, "Status"); ui.wifi_status_value = lv_label_create(page); lv_obj_set_pos(ui.wifi_status_value, 118, 94); lv_obj_set_width(ui.wifi_status_value, 440); lv_obj_set_style_text_color(ui.wifi_status_value, lv_color_hex(0xF5FAFC), 0); lv_obj_set_style_text_font(ui.wifi_status_value, &lv_font_montserrat_18, 0); lv_label_set_long_mode(ui.wifi_status_value, LV_LABEL_LONG_WRAP); lv_label_set_text(ui.wifi_status_value, "Not configured"); lv_obj_t *ip_label = lv_label_create(page); lv_obj_set_pos(ip_label, 24, 132); lv_obj_set_style_text_color(ip_label, lv_color_hex(0x7FA1B2), 0); lv_obj_set_style_text_font(ip_label, &lv_font_montserrat_14, 0); lv_label_set_text(ip_label, "IP"); ui.wifi_ip_value = lv_label_create(page); lv_obj_set_pos(ui.wifi_ip_value, 118, 128); lv_obj_set_width(ui.wifi_ip_value, 440); lv_obj_set_style_text_color(ui.wifi_ip_value, lv_color_hex(0xF5FAFC), 0); lv_obj_set_style_text_font(ui.wifi_ip_value, &lv_font_montserrat_18, 0); lv_label_set_text(ui.wifi_ip_value, "--"); lv_obj_t *ssid_label = lv_label_create(page); lv_obj_set_pos(ssid_label, 24, 182); lv_obj_set_style_text_color(ssid_label, lv_color_hex(0x7FA1B2), 0); lv_obj_set_style_text_font(ssid_label, &lv_font_montserrat_14, 0); lv_label_set_text(ssid_label, "SSID"); ui.wifi_ssid_ta = createTextInput(page, 24, 208, 536, "Wi-Fi SSID", "Network name", false); if (!wifi_data.ssid.isEmpty()) { lv_textarea_set_text(ui.wifi_ssid_ta, wifi_data.ssid.c_str()); } lv_obj_t *password_label = lv_label_create(page); lv_obj_set_pos(password_label, 24, 268); lv_obj_set_style_text_color(password_label, lv_color_hex(0x7FA1B2), 0); lv_obj_set_style_text_font(password_label, &lv_font_montserrat_14, 0); lv_label_set_text(password_label, "Password"); ui.wifi_password_ta = createTextInput(page, 24, 294, 536, "Wi-Fi Password", "Password", true); if (!wifi_data.password.isEmpty()) { lv_textarea_set_text(ui.wifi_password_ta, wifi_data.password.c_str()); } createActionButton(page, "Connect", 24, 356, 154, 58, lv_color_hex(0x245C7A), wifiConnectButtonEvent); createActionButton(page, "Save + Connect", 198, 356, 186, 58, lv_color_hex(0x28714A), wifiSaveConnectButtonEvent); createActionButton(page, "Disconnect", 404, 356, 156, 58, lv_color_hex(0x7A3A2A), wifiDisconnectButtonEvent); ui.wifi_note_label = lv_label_create(page); lv_obj_set_pos(ui.wifi_note_label, 24, 420); lv_obj_set_width(ui.wifi_note_label, 550); lv_obj_set_style_text_color(ui.wifi_note_label, lv_color_hex(0x8FA3AC), 0); lv_obj_set_style_text_font(ui.wifi_note_label, &lv_font_montserrat_14, 0); lv_label_set_long_mode(ui.wifi_note_label, LV_LABEL_LONG_WRAP); lv_label_set_text(ui.wifi_note_label, "Enter Wi-Fi credentials, then tap Connect or Save + Connect."); } void buildInventreePage() { lv_obj_t *page = createPageContainer(ui.content_panel); ui.pages[pageIndex(PageId::Inventree)] = page; lv_obj_t *title = lv_label_create(page); lv_obj_set_pos(title, 24, 18); lv_obj_set_style_text_color(title, lv_color_hex(0xF5FAFC), 0); lv_obj_set_style_text_font(title, &lv_font_montserrat_20, 0); lv_label_set_text(title, "InvenTree"); lv_obj_t *subtitle = lv_label_create(page); lv_obj_set_pos(subtitle, 24, 48); lv_obj_set_style_text_color(subtitle, lv_color_hex(0x6F8893), 0); lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_14, 0); lv_label_set_text(subtitle, "Find stock by Batch Code and write the measured weight into stock quantity"); lv_obj_t *url_label = lv_label_create(page); lv_obj_set_pos(url_label, 24, 88); lv_obj_set_style_text_color(url_label, lv_color_hex(0x7FA1B2), 0); lv_obj_set_style_text_font(url_label, &lv_font_montserrat_14, 0); lv_label_set_text(url_label, "Base URL"); ui.inventree_url_ta = createTextInput(page, 24, 112, 250, "InvenTree Base URL", kDefaultInventreeBaseUrl, false); if (!inventree_data.base_url.isEmpty()) { lv_textarea_set_text(ui.inventree_url_ta, inventree_data.base_url.c_str()); } lv_obj_t *token_label = lv_label_create(page); lv_obj_set_pos(token_label, 24, 170); lv_obj_set_style_text_color(token_label, lv_color_hex(0x7FA1B2), 0); lv_obj_set_style_text_font(token_label, &lv_font_montserrat_14, 0); lv_label_set_text(token_label, "API Token (opt)"); ui.inventree_token_ta = createTextInput(page, 24, 194, 250, "InvenTree API Token", "Token", true); if (!inventree_data.token.isEmpty()) { lv_textarea_set_text(ui.inventree_token_ta, inventree_data.token.c_str()); } lv_obj_t *batch_label = lv_label_create(page); lv_obj_set_pos(batch_label, 24, 252); lv_obj_set_style_text_color(batch_label, lv_color_hex(0x7FA1B2), 0); lv_obj_set_style_text_font(batch_label, &lv_font_montserrat_14, 0); lv_label_set_text(batch_label, "Batch Code"); ui.inventree_batch_ta = createTextInput(page, 24, 276, 250, "InvenTree Batch Code", "Batch Code", false); if (!inventree_data.batch.isEmpty()) { lv_textarea_set_text(ui.inventree_batch_ta, inventree_data.batch.c_str()); } lv_obj_t *status_label = lv_label_create(page); lv_obj_set_pos(status_label, 304, 88); lv_obj_set_style_text_color(status_label, lv_color_hex(0x7FA1B2), 0); lv_obj_set_style_text_font(status_label, &lv_font_montserrat_14, 0); lv_label_set_text(status_label, "Status"); ui.inventree_status_value = lv_label_create(page); lv_obj_set_pos(ui.inventree_status_value, 304, 112); lv_obj_set_width(ui.inventree_status_value, 280); lv_obj_set_style_text_color(ui.inventree_status_value, lv_color_hex(0xF5FAFC), 0); lv_obj_set_style_text_font(ui.inventree_status_value, &lv_font_montserrat_18, 0); lv_label_set_long_mode(ui.inventree_status_value, LV_LABEL_LONG_WRAP); lv_label_set_text(ui.inventree_status_value, "Not configured"); lv_obj_t *result_label = lv_label_create(page); lv_obj_set_pos(result_label, 304, 170); lv_obj_set_style_text_color(result_label, lv_color_hex(0x7FA1B2), 0); lv_obj_set_style_text_font(result_label, &lv_font_montserrat_14, 0); lv_label_set_text(result_label, "Selected Stock Item"); ui.inventree_result_value = lv_label_create(page); lv_obj_set_pos(ui.inventree_result_value, 304, 194); lv_obj_set_width(ui.inventree_result_value, 280); lv_obj_set_style_text_color(ui.inventree_result_value, lv_color_hex(0xF5FAFC), 0); lv_obj_set_style_text_font(ui.inventree_result_value, &lv_font_montserrat_14, 0); lv_label_set_long_mode(ui.inventree_result_value, LV_LABEL_LONG_WRAP); lv_label_set_text(ui.inventree_result_value, "No stock item selected."); createActionButton(page, "Save", 24, 340, 118, 46, lv_color_hex(0x245C7A), inventreeSaveButtonEvent); createActionButton(page, "Test API", 156, 340, 118, 46, lv_color_hex(0x28714A), inventreeTestButtonEvent); createActionButton(page, "Find", 24, 394, 118, 46, lv_color_hex(0x245C7A), inventreeFindButtonEvent); createActionButton(page, "Next", 156, 394, 118, 46, lv_color_hex(0x3D4E5F), inventreeNextButtonEvent); createActionButton(page, "Push Weight", 304, 394, 280, 46, lv_color_hex(0x7A3A2A), inventreePushButtonEvent); ui.inventree_note_label = lv_label_create(page); lv_obj_set_pos(ui.inventree_note_label, 304, 332); lv_obj_set_width(ui.inventree_note_label, 280); lv_obj_set_style_text_color(ui.inventree_note_label, lv_color_hex(0x8FA3AC), 0); lv_obj_set_style_text_font(ui.inventree_note_label, &lv_font_montserrat_14, 0); lv_label_set_long_mode(ui.inventree_note_label, LV_LABEL_LONG_WRAP); lv_label_set_text(ui.inventree_note_label, "Save URL, test API, find by Batch Code, then push the measured weight."); } void buildUi() { ui.screen = lv_obj_create(nullptr); lv_obj_remove_style_all(ui.screen); lv_obj_set_style_bg_color(ui.screen, lv_color_black(), 0); lv_obj_set_style_bg_grad_color(ui.screen, lv_color_black(), 0); ui.nav_panel = lv_obj_create(ui.screen); lv_obj_set_size(ui.nav_panel, 156, 456); lv_obj_set_pos(ui.nav_panel, 12, 12); lv_obj_remove_flag(ui.nav_panel, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_radius(ui.nav_panel, 24, 0); lv_obj_set_style_bg_color(ui.nav_panel, lv_color_black(), 0); lv_obj_set_style_bg_grad_color(ui.nav_panel, lv_color_black(), 0); lv_obj_set_style_border_color(ui.nav_panel, lv_color_hex(0x161616), 0); lv_obj_set_style_border_width(ui.nav_panel, 1, 0); lv_obj_set_style_shadow_width(ui.nav_panel, 0, 0); lv_obj_t *nav_title = lv_label_create(ui.nav_panel); lv_obj_set_pos(nav_title, 18, 24); lv_obj_set_width(nav_title, 120); lv_obj_set_style_text_color(nav_title, lv_color_hex(0xF5FAFC), 0); lv_obj_set_style_text_font(nav_title, &lv_font_montserrat_20, 0); lv_obj_set_style_text_align(nav_title, LV_TEXT_ALIGN_CENTER, 0); lv_label_set_text(nav_title, "Menu"); createNavButton(ui.nav_panel, "Main", 96, navWeightEvent, PageId::Weight); createNavButton(ui.nav_panel, "Cal", 166, navCalibrationEvent, PageId::Calibration); createNavButton(ui.nav_panel, "Wi-Fi", 236, navWifiEvent, PageId::Wifi); createNavButton(ui.nav_panel, "Inv", 306, navInventreeEvent, PageId::Inventree); ui.content_panel = lv_obj_create(ui.screen); lv_obj_set_size(ui.content_panel, 608, 456); lv_obj_set_pos(ui.content_panel, 180, 12); lv_obj_remove_flag(ui.content_panel, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_radius(ui.content_panel, 24, 0); lv_obj_set_style_bg_color(ui.content_panel, lv_color_black(), 0); lv_obj_set_style_bg_grad_color(ui.content_panel, lv_color_black(), 0); lv_obj_set_style_border_color(ui.content_panel, lv_color_hex(0x161616), 0); lv_obj_set_style_border_width(ui.content_panel, 1, 0); lv_obj_set_style_shadow_width(ui.content_panel, 0, 0); lv_obj_set_style_pad_all(ui.content_panel, 0, 0); buildWeightPage(); buildCalibrationPage(); buildWifiPage(); buildInventreePage(); ui.text_editor_overlay = lv_obj_create(ui.screen); lv_obj_remove_flag(ui.text_editor_overlay, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_size(ui.text_editor_overlay, 800, 480); lv_obj_set_pos(ui.text_editor_overlay, 0, 0); lv_obj_set_style_radius(ui.text_editor_overlay, 0, 0); lv_obj_set_style_bg_color(ui.text_editor_overlay, lv_color_black(), 0); lv_obj_set_style_bg_opa(ui.text_editor_overlay, LV_OPA_80, 0); lv_obj_set_style_border_width(ui.text_editor_overlay, 0, 0); lv_obj_set_style_pad_all(ui.text_editor_overlay, 0, 0); lv_obj_add_flag(ui.text_editor_overlay, LV_OBJ_FLAG_CLICKABLE); lv_obj_add_flag(ui.text_editor_overlay, LV_OBJ_FLAG_HIDDEN); ui.text_editor_panel = lv_obj_create(ui.text_editor_overlay); lv_obj_remove_flag(ui.text_editor_panel, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_size(ui.text_editor_panel, 776, 112); lv_obj_set_pos(ui.text_editor_panel, 12, 12); lv_obj_set_style_radius(ui.text_editor_panel, 24, 0); lv_obj_set_style_bg_color(ui.text_editor_panel, lv_color_black(), 0); lv_obj_set_style_bg_grad_color(ui.text_editor_panel, lv_color_black(), 0); lv_obj_set_style_border_color(ui.text_editor_panel, lv_color_hex(0x1A1A1A), 0); lv_obj_set_style_border_width(ui.text_editor_panel, 1, 0); lv_obj_set_style_shadow_width(ui.text_editor_panel, 0, 0); lv_obj_set_style_pad_all(ui.text_editor_panel, 0, 0); ui.text_editor_title = lv_label_create(ui.text_editor_panel); lv_obj_set_pos(ui.text_editor_title, 24, 14); lv_obj_set_width(ui.text_editor_title, 320); lv_obj_set_style_text_color(ui.text_editor_title, lv_color_hex(0xF5FAFC), 0); lv_obj_set_style_text_font(ui.text_editor_title, &lv_font_montserrat_20, 0); lv_label_set_text(ui.text_editor_title, "Edit"); createActionButton(ui.text_editor_panel, "Cancel", 528, 8, 108, 38, lv_color_hex(0x323232), textEditorCancelButtonEvent); createActionButton(ui.text_editor_panel, "OK", 648, 8, 104, 38, lv_color_hex(0x245C7A), textEditorApplyButtonEvent); ui.text_editor_ta = lv_textarea_create(ui.text_editor_panel); lv_obj_set_size(ui.text_editor_ta, 728, 44); lv_obj_set_pos(ui.text_editor_ta, 24, 58); lv_obj_set_style_radius(ui.text_editor_ta, 14, 0); lv_obj_set_style_bg_color(ui.text_editor_ta, lv_color_black(), 0); lv_obj_set_style_border_color(ui.text_editor_ta, lv_color_hex(0x2A2A2A), 0); lv_obj_set_style_border_width(ui.text_editor_ta, 1, 0); lv_obj_set_style_text_color(ui.text_editor_ta, lv_color_hex(0xF5FAFC), 0); lv_obj_set_style_pad_hor(ui.text_editor_ta, 16, 0); lv_obj_set_style_pad_ver(ui.text_editor_ta, 8, 0); lv_textarea_set_one_line(ui.text_editor_ta, true); ui.wifi_keyboard = lv_keyboard_create(ui.text_editor_overlay); lv_obj_set_size(ui.wifi_keyboard, 776, 336); lv_obj_set_pos(ui.wifi_keyboard, 12, 132); lv_obj_set_style_radius(ui.wifi_keyboard, 24, LV_PART_MAIN); lv_obj_set_style_bg_color(ui.wifi_keyboard, lv_color_black(), LV_PART_MAIN); lv_obj_set_style_bg_grad_color(ui.wifi_keyboard, lv_color_black(), LV_PART_MAIN); lv_obj_set_style_border_color(ui.wifi_keyboard, lv_color_hex(0x1A1A1A), LV_PART_MAIN); lv_obj_set_style_border_width(ui.wifi_keyboard, 1, LV_PART_MAIN); lv_obj_set_style_shadow_width(ui.wifi_keyboard, 0, LV_PART_MAIN); lv_obj_set_style_pad_all(ui.wifi_keyboard, 4, LV_PART_MAIN); lv_obj_set_style_pad_row(ui.wifi_keyboard, 4, LV_PART_MAIN); lv_obj_set_style_pad_column(ui.wifi_keyboard, 4, LV_PART_MAIN); lv_obj_set_style_radius(ui.wifi_keyboard, 12, LV_PART_ITEMS); lv_obj_set_style_border_width(ui.wifi_keyboard, 0, LV_PART_ITEMS); lv_obj_set_style_shadow_width(ui.wifi_keyboard, 0, LV_PART_ITEMS); lv_keyboard_set_mode(ui.wifi_keyboard, LV_KEYBOARD_MODE_TEXT_LOWER); lv_obj_add_event_cb(ui.wifi_keyboard, textEditorKeyboardEvent, LV_EVENT_ALL, nullptr); lv_screen_load(ui.screen); setWeightNote("Enter Batch Code, confirm input, then tap Write Weight."); setCalibrationNote("Empty the platform, tap Tare, place the 100 g reference weight, then tap Cal."); if (wifi_data.ssid.isEmpty()) { setWifiNote("Tap a field to edit fullscreen, then Connect or Save + Connect."); } else { setWifiNote("Saved credentials loaded. Tap a field to edit fullscreen, then Connect."); } if (inventreeConfigured()) { inventree_data.status_text = "Config loaded"; inventree_data.result_text = inventree_data.token.isEmpty() ? "Saved URL and Batch Code loaded. Token will be requested automatically." : "Saved URL, token and Batch Code loaded from device storage."; setInventreeNote("Use Test API, then Find by Batch Code and Push Weight. API token is optional."); } else { inventree_data.status_text = "Not configured"; inventree_data.result_text = "Base URL is prefilled. Enter Batch Code. Token is optional."; setInventreeNote("Tap a field to edit fullscreen, then Save, Test API, Find and Push Weight."); } setActivePage(PageId::Weight); refreshUi(); updateWifiUi(true); updateInventreeLabels(); } void handleSerialCommand(const String &command) { if (command.equalsIgnoreCase("tare")) { performTare(); return; } if (command.equalsIgnoreCase("reset")) { clearCalibration(); return; } if (command.equalsIgnoreCase("status")) { Serial.printf("online=%d raw=%ld offset=%ld scale=%f has_offset=%d wifi_status=%d\n", sensor_online ? 1 : 0, static_cast(last_raw), static_cast(calibration.offset), calibration.scale, calibration.has_offset ? 1 : 0, static_cast(WiFi.status())); return; } if (command.startsWith("cal ")) { const float grams = command.substring(4).toFloat(); performCalibration(grams); return; } if (command.startsWith("scale ")) { const float scale = command.substring(6).toFloat(); if (scale > 0.0f) { calibration.scale = scale; saveCalibration(); displayed_weight_valid = false; refreshUi(); setCalibrationNote("Manual scale factor saved."); } else { setCalibrationNote("Invalid scale factor."); } return; } setCalibrationNote("Unknown serial command. Use: tare | cal | scale | reset | status"); } void handleSerial() { while (Serial.available() > 0) { const char ch = static_cast(Serial.read()); if (ch == '\r') { continue; } if (ch == '\n') { serial_line.trim(); if (!serial_line.isEmpty()) { handleSerialCommand(serial_line); } serial_line = ""; continue; } serial_line += ch; } } void sampleSensor() { if (!hx711.isReady()) { if (millis() - last_sample_ms > kSensorTimeoutMs) { sensor_online = false; displayed_weight_valid = false; } return; } last_raw = hx711.readRaw(); pushRawSample(last_raw); const float averaged_raw = getAveragedRawSample(); if (!have_sample) { filtered_raw = averaged_raw; have_sample = true; } else { filtered_raw += kRawFilterAlpha * (averaged_raw - filtered_raw); } sensor_online = true; last_sample_ms = millis(); } } // namespace void setup() { #ifdef ARDUINO_USB_CDC_ON_BOOT delay(5000); #endif Serial.begin(115200); Serial.setDebugOutput(true); log_i("Board: %s", BOARD_NAME); log_i("CPU: %s rev%d, %d MHz, %d core(s)", ESP.getChipModel(), ESP.getChipRevision(), getCpuFrequencyMhz(), ESP.getChipCores()); log_i("Free heap: %d bytes", ESP.getFreeHeap()); log_i("Free PSRAM: %d bytes", ESP.getPsramSize()); log_i("SDK version: %s", ESP.getSdkVersion()); prefs.begin(kPrefsNamespace, false); loadCalibration(); loadWifiConfig(); loadInventreeConfig(); hx711.begin(HX711_DOUT_PIN, HX711_SCK_PIN); smartdisplay_init(); auto *display = lv_disp_get_default(); lv_display_set_rotation(display, LV_DISPLAY_ROTATION_180); buildUi(); WiFi.mode(WIFI_OFF); if (!wifi_data.ssid.isEmpty()) { connectWifi(wifi_data.ssid, wifi_data.password, false); setWifiNote("Auto-connecting to saved Wi-Fi..."); } last_lv_tick_ms = millis(); next_ui_refresh_ms = millis(); log_i("HX711 pins: SCK=%d DOUT=%d", HX711_SCK_PIN, HX711_DOUT_PIN); } void loop() { const uint32_t now = millis(); sampleSensor(); handleSerial(); handleWifi(); if (now >= next_ui_refresh_ms) { next_ui_refresh_ms = now + kUiRefreshMs; refreshUi(); } lv_tick_inc(now - last_lv_tick_ms); last_lv_tick_ms = now; lv_timer_handler(); delay(5); }