розрізвно на декілька файлів
This commit is contained in:
206
src/app.h
Normal file
206
src/app.h
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
#ifndef APP_H
|
||||||
|
#define APP_H
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <esp32_smartdisplay.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
|
||||||
|
|
||||||
|
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;
|
||||||
|
constexpr lv_coord_t kScreenMargin = 12;
|
||||||
|
constexpr lv_coord_t kPanelRadius = 24;
|
||||||
|
constexpr lv_coord_t kContentPadding = 24;
|
||||||
|
constexpr lv_coord_t kContentWidth = 608;
|
||||||
|
constexpr lv_coord_t kContentHeight = 456;
|
||||||
|
constexpr lv_coord_t kContentInnerWidth = kContentWidth - (kContentPadding * 2);
|
||||||
|
constexpr lv_coord_t kTextEditorPanelHeight = 112;
|
||||||
|
constexpr lv_coord_t kTextEditorGap = 12;
|
||||||
|
constexpr lv_coord_t kTextEditorMinKeyboardHeight = 240;
|
||||||
|
|
||||||
|
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_cancel_button = nullptr;
|
||||||
|
lv_obj_t *text_editor_apply_button = 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern Hx711Sensor hx711;
|
||||||
|
extern Preferences prefs;
|
||||||
|
extern CalibrationData calibration;
|
||||||
|
extern WifiData wifi_data;
|
||||||
|
extern InventreeData inventree_data;
|
||||||
|
extern UiRefs ui;
|
||||||
|
|
||||||
|
extern PageId active_page;
|
||||||
|
extern bool have_sample;
|
||||||
|
extern bool sensor_online;
|
||||||
|
extern float filtered_raw;
|
||||||
|
extern int32_t last_raw;
|
||||||
|
extern int32_t raw_samples[kRawFilterWindow];
|
||||||
|
extern uint8_t raw_sample_count;
|
||||||
|
extern uint8_t raw_sample_index;
|
||||||
|
extern int32_t displayed_weight_grams;
|
||||||
|
extern bool displayed_weight_valid;
|
||||||
|
extern uint32_t last_sample_ms;
|
||||||
|
extern uint32_t next_ui_refresh_ms;
|
||||||
|
extern uint32_t last_lv_tick_ms;
|
||||||
|
|
||||||
|
constexpr size_t pageIndex(PageId page)
|
||||||
|
{
|
||||||
|
return static_cast<size_t>(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveCalibration();
|
||||||
|
void loadCalibration();
|
||||||
|
void saveWifiConfig();
|
||||||
|
void loadWifiConfig();
|
||||||
|
void saveInventreeConfig();
|
||||||
|
void loadInventreeConfig();
|
||||||
|
|
||||||
|
float getCurrentWeightGrams();
|
||||||
|
void performTare();
|
||||||
|
void performCalibration(float reference_grams);
|
||||||
|
void clearCalibration();
|
||||||
|
void sampleSensor();
|
||||||
|
|
||||||
|
void connectWifi(const String &ssid, const String &password, bool save_credentials);
|
||||||
|
void connectWifiFromForm(bool save_credentials);
|
||||||
|
void disconnectWifi();
|
||||||
|
void handleWifi();
|
||||||
|
bool inventreeConfigured();
|
||||||
|
void clearInventreeMatches();
|
||||||
|
void saveInventreeSettingsFromForm();
|
||||||
|
void testInventreeApi();
|
||||||
|
void findInventreeStockByBatch();
|
||||||
|
void selectInventreeMatch(int delta);
|
||||||
|
void pushMeasuredWeightToInventree();
|
||||||
|
|
||||||
|
void setLabelTextIfChanged(lv_obj_t *label, const char *text);
|
||||||
|
void setWeightStatus(const char *text, lv_color_t color);
|
||||||
|
void setWeightNote(const char *text);
|
||||||
|
void setCalibrationNote(const char *text);
|
||||||
|
void setWifiNote(const char *text);
|
||||||
|
void setInventreeNote(const char *text);
|
||||||
|
void hideKeyboard();
|
||||||
|
void refreshUi();
|
||||||
|
void updateWifiUi(bool force = false);
|
||||||
|
void updateInventreeLabels();
|
||||||
|
void setActivePage(PageId page);
|
||||||
|
void buildUi();
|
||||||
|
|
||||||
|
#endif
|
||||||
22
src/app_context.cpp
Normal file
22
src/app_context.cpp
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#include "app.h"
|
||||||
|
|
||||||
|
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;
|
||||||
801
src/connectivity.cpp
Normal file
801
src/connectivity.cpp
Normal file
@@ -0,0 +1,801 @@
|
|||||||
|
#include "app.h"
|
||||||
|
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
#include <WiFiClient.h>
|
||||||
|
#include <WiFiClientSecure.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 int32_t stable_grams = displayed_weight_valid ? displayed_weight_grams : static_cast<int32_t>(lroundf(getCurrentWeightGrams()));
|
||||||
|
quantity_text = String(stable_grams < 0 ? 0 : stable_grams);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
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", ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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();
|
||||||
|
bool force_refresh = false;
|
||||||
|
if (status == WL_CONNECTED)
|
||||||
|
{
|
||||||
|
if (wifi_data.connecting)
|
||||||
|
{
|
||||||
|
wifi_data.connecting = false;
|
||||||
|
setWifiNote("Wi-Fi connected.");
|
||||||
|
force_refresh = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.");
|
||||||
|
force_refresh = true;
|
||||||
|
}
|
||||||
|
else if (status != wifi_data.last_status)
|
||||||
|
{
|
||||||
|
if (status == WL_CONNECT_FAILED)
|
||||||
|
{
|
||||||
|
wifi_data.connecting = false;
|
||||||
|
setWifiNote("Connection failed. Check password.");
|
||||||
|
force_refresh = true;
|
||||||
|
}
|
||||||
|
else if (status == WL_NO_SSID_AVAIL)
|
||||||
|
{
|
||||||
|
wifi_data.connecting = false;
|
||||||
|
setWifiNote("SSID not found.");
|
||||||
|
force_refresh = true;
|
||||||
|
}
|
||||||
|
else if (status == WL_CONNECTION_LOST)
|
||||||
|
{
|
||||||
|
wifi_data.connecting = false;
|
||||||
|
setWifiNote("Wi-Fi connection lost.");
|
||||||
|
force_refresh = true;
|
||||||
|
}
|
||||||
|
else if (status == WL_DISCONNECTED && !wifi_data.connecting && !wifi_data.ssid.isEmpty())
|
||||||
|
{
|
||||||
|
setWifiNote("Wi-Fi disconnected.");
|
||||||
|
force_refresh = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWifiUi(force_refresh || status != wifi_data.last_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveInventreeSettingsFromForm()
|
||||||
|
{
|
||||||
|
syncInventreeInputs(true);
|
||||||
|
clearInventreeMatches();
|
||||||
|
|
||||||
|
if (!inventreeConfigured())
|
||||||
|
{
|
||||||
|
inventree_data.status_text = "Settings saved";
|
||||||
|
inventree_data.result_text = "Saved current values. Add Base URL to enable API requests.";
|
||||||
|
setInventreeNote("Settings are stored on the device, but Base URL is still required for API access.");
|
||||||
|
}
|
||||||
|
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.");
|
||||||
|
}
|
||||||
2277
src/main.cpp
2277
src/main.cpp
File diff suppressed because it is too large
Load Diff
227
src/sensor_logic.cpp
Normal file
227
src/sensor_logic.cpp
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
#include "app.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateDisplayedWeight()
|
||||||
|
{
|
||||||
|
if (!sensor_online || !calibration.has_offset || calibration.scale <= 0.0001f)
|
||||||
|
{
|
||||||
|
displayed_weight_valid = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyWeightHysteresis(getCurrentWeightGrams());
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
float getCurrentWeightGrams()
|
||||||
|
{
|
||||||
|
return (filtered_raw - static_cast<float>(calibration.offset)) / calibration.scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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();
|
||||||
|
updateDisplayedWeight();
|
||||||
|
}
|
||||||
1183
src/ui.cpp
Normal file
1183
src/ui.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user