2429 lines
78 KiB
C++
2429 lines
78 KiB
C++
#include <Arduino.h>
|
|
#include <ArduinoJson.h>
|
|
#include <HTTPClient.h>
|
|
#include <Preferences.h>
|
|
#include <WiFi.h>
|
|
#include <WiFiClient.h>
|
|
#include <WiFiClientSecure.h>
|
|
#include <esp32_smartdisplay.h>
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#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<size_t>(PageId::Count)] = {};
|
|
lv_obj_t *nav_labels[static_cast<size_t>(PageId::Count)] = {};
|
|
lv_obj_t *pages[static_cast<size_t>(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<size_t>(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<double>(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<float>(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<float>(last_raw);
|
|
}
|
|
|
|
int64_t total = 0;
|
|
for (uint8_t i = 0; i < raw_sample_count; ++i)
|
|
{
|
|
total += raw_samples[i];
|
|
}
|
|
|
|
return static_cast<float>(total) / static_cast<float>(raw_sample_count);
|
|
}
|
|
|
|
int32_t applyWeightHysteresis(float grams)
|
|
{
|
|
if (!displayed_weight_valid)
|
|
{
|
|
displayed_weight_grams = static_cast<int32_t>(lroundf(grams));
|
|
displayed_weight_valid = true;
|
|
return displayed_weight_grams;
|
|
}
|
|
|
|
while (grams >= static_cast<float>(displayed_weight_grams) + kWeightStepHysteresisGrams)
|
|
{
|
|
++displayed_weight_grams;
|
|
}
|
|
|
|
while (grams <= static_cast<float>(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<long>(calibration.offset));
|
|
setLabelLongText(ui.cal_raw_value, "%ld", static_cast<long>(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<long>(lroundf(filtered_raw)));
|
|
setLabelLongText(ui.cal_raw_value, "%ld", static_cast<long>(lroundf(filtered_raw)));
|
|
|
|
if (calibration.scale > 0.0001f && calibration.has_offset)
|
|
{
|
|
const float grams = (filtered_raw - static_cast<float>(calibration.offset)) / calibration.scale;
|
|
const int32_t stable_grams = applyWeightHysteresis(grams);
|
|
setLabelLongText(ui.weight_label, "%ld", static_cast<long>(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<long>(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<long>(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<float>(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<PageId>(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<uint8_t>(value[i]);
|
|
if ((ch >= 'a' && ch <= 'z') ||
|
|
(ch >= 'A' && ch <= 'Z') ||
|
|
(ch >= '0' && ch <= '9') ||
|
|
ch == '-' || ch == '_' || ch == '.' || ch == '~')
|
|
{
|
|
encoded += static_cast<char>(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 <typename TClient>
|
|
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<float>(calibration.offset)) / calibration.scale;
|
|
const int32_t stable_grams = displayed_weight_valid ? displayed_weight_grams : static_cast<int32_t>(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<JsonArray>();
|
|
JsonObject filter_item = filter_results.add<JsonObject>();
|
|
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<JsonArray>();
|
|
inventree_data.total_matches = doc["count"] | static_cast<int>(results.size());
|
|
inventree_data.stored_matches = 0;
|
|
|
|
for (JsonObject item : results)
|
|
{
|
|
if (inventree_data.stored_matches >= static_cast<int>(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<const char *>(item["batch"] | ""));
|
|
match.part_name = String(static_cast<const char *>(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<JsonArray>();
|
|
JsonObject item = items.add<JsonObject>();
|
|
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_obj_t *>(lv_event_get_target(event));
|
|
const char *title = static_cast<const char *>(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<char *>(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<double>(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<long>(last_raw),
|
|
static_cast<long>(calibration.offset),
|
|
calibration.scale,
|
|
calibration.has_offset ? 1 : 0,
|
|
static_cast<int>(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 <grams> | scale <cnt_per_g> | reset | status");
|
|
}
|
|
|
|
void handleSerial()
|
|
{
|
|
while (Serial.available() > 0)
|
|
{
|
|
const char ch = static_cast<char>(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);
|
|
}
|