From 2f2998b0fdc7442266c20f7bdb65bfd2f1acd6ce Mon Sep 17 00:00:00 2001 From: shironeko Date: Sat, 7 Mar 2026 19:21:14 -0300 Subject: [PATCH] esp attempt, stock + unit type --- SekiScaleESP/.gitignore | 5 ++ SekiScaleESP/.vscode/extensions.json | 10 +++ SekiScaleESP/include/README | 37 +++++++++ SekiScaleESP/lib/README | 46 +++++++++++ SekiScaleESP/platformio.ini | 16 ++++ SekiScaleESP/src/main.cpp | 39 +++++++++ SekiScaleESP/test/README | 11 +++ app.py | 59 +++++++++++--- templates/index.html | 113 +++++++++++++++++---------- 9 files changed, 283 insertions(+), 53 deletions(-) create mode 100644 SekiScaleESP/.gitignore create mode 100644 SekiScaleESP/.vscode/extensions.json create mode 100644 SekiScaleESP/include/README create mode 100644 SekiScaleESP/lib/README create mode 100644 SekiScaleESP/platformio.ini create mode 100644 SekiScaleESP/src/main.cpp create mode 100644 SekiScaleESP/test/README diff --git a/SekiScaleESP/.gitignore b/SekiScaleESP/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/SekiScaleESP/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/SekiScaleESP/.vscode/extensions.json b/SekiScaleESP/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/SekiScaleESP/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/SekiScaleESP/include/README b/SekiScaleESP/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/SekiScaleESP/include/README @@ -0,0 +1,37 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the convention is to give header files names that end with `.h'. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/SekiScaleESP/lib/README b/SekiScaleESP/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/SekiScaleESP/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into the executable file. + +The source code of each library should be placed in a separate directory +("lib/your_library_name/[Code]"). + +For example, see the structure of the following example libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +Example contents of `src/main.c` using Foo and Bar: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +The PlatformIO Library Dependency Finder will find automatically dependent +libraries by scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/SekiScaleESP/platformio.ini b/SekiScaleESP/platformio.ini new file mode 100644 index 0000000..5e22d5f --- /dev/null +++ b/SekiScaleESP/platformio.ini @@ -0,0 +1,16 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:d1] +platform = espressif8266 +board = d1 +framework = arduino +monitor_speed = 115200 +upload_port = /dev/ttyUSB1 \ No newline at end of file diff --git a/SekiScaleESP/src/main.cpp b/SekiScaleESP/src/main.cpp new file mode 100644 index 0000000..fbd9447 --- /dev/null +++ b/SekiScaleESP/src/main.cpp @@ -0,0 +1,39 @@ +#include + +const int triggerPin = D2; +bool lastState = HIGH; + +void setup() { + Serial.begin(115200); + randomSeed(analogRead(0)); + + pinMode(LED_BUILTIN, OUTPUT); + pinMode(triggerPin, INPUT_PULLUP); + + // Flash LED to signal boot + digitalWrite(LED_BUILTIN, LOW); + delay(500); + digitalWrite(LED_BUILTIN, HIGH); +} + +void loop() { + bool currentState = digitalRead(triggerPin); + + // Detect when D4 touches GND (Falling Edge) + if (currentState == LOW && lastState == HIGH) { + int randomWeight = random(100, 5000); + + // Output formatted for easy parsing + Serial.print("WEIGHT:"); + Serial.println(randomWeight); + + // Visual feedback + digitalWrite(LED_BUILTIN, LOW); + delay(100); + digitalWrite(LED_BUILTIN, HIGH); + + delay(250); // Debounce + } + + lastState = currentState; +} \ No newline at end of file diff --git a/SekiScaleESP/test/README b/SekiScaleESP/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/SekiScaleESP/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/app.py b/app.py index 2b08d6e..afa7183 100644 --- a/app.py +++ b/app.py @@ -38,10 +38,16 @@ def init_db(): with sqlite3.connect(DB_FILE) as conn: conn.execute('''CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT UNIQUE, password TEXT)''') + # Updated table definition conn.execute('''CREATE TABLE IF NOT EXISTS products - (barcode TEXT PRIMARY KEY, name TEXT, price REAL, image_url TEXT)''') + (barcode TEXT PRIMARY KEY, + name TEXT, + price REAL, + image_url TEXT, + stock REAL DEFAULT 0, + unit_type TEXT DEFAULT 'unit')''') - # Default user: admin / Pass: choripan1234 + # Default user logic remains same... user = conn.execute('SELECT * FROM users WHERE username = ?', ('admin',)).fetchone() if not user: hashed_pw = generate_password_hash('choripan1234') @@ -143,16 +149,25 @@ def upsert(): try: price = float(d['price']) + stock = float(d.get('stock', 0)) # New field except (ValueError, TypeError): price = 0.0 + stock = 0.0 + unit_type = d.get('unit_type', 'unit') # New field (unit or kg) final_image_path = download_image(d['image_url'], barcode) with sqlite3.connect(DB_FILE) as conn: - conn.execute('''INSERT INTO products (barcode, name, price, image_url) VALUES (?,?,?,?) - ON CONFLICT(barcode) DO UPDATE SET name=excluded.name, - price=excluded.price, image_url=excluded.image_url''', - (barcode, d['name'], price, final_image_path)) + # Updated UPSERT query + conn.execute('''INSERT INTO products (barcode, name, price, image_url, stock, unit_type) + VALUES (?,?,?,?,?,?) + ON CONFLICT(barcode) DO UPDATE SET + name=excluded.name, + price=excluded.price, + image_url=excluded.image_url, + stock=excluded.stock, + unit_type=excluded.unit_type''', + (barcode, d['name'], price, final_image_path, stock, unit_type)) conn.commit() return redirect(url_for('index')) @@ -175,14 +190,13 @@ def scan(): return jsonify({"status": "error", "message": "empty barcode"}), 400 with sqlite3.connect(DB_FILE) as conn: - # Specifically select the 4 columns the code expects - p = conn.execute('SELECT barcode, name, price, image_url FROM products WHERE barcode = ?', (barcode,)).fetchone() + # Fixed: Selecting all 6 necessary columns + p = conn.execute('SELECT barcode, name, price, image_url, stock, unit_type FROM products WHERE barcode = ?', (barcode,)).fetchone() if p: - # Now this will always have exactly 4 values, regardless of DB changes - barcode_val, name, price, image_path = p + # Now matches the 6 columns in the SELECT statement + barcode_val, name, price, image_path, stock, unit_type = p - # Image recovery logic for missing local files if image_path and image_path.startswith('/static/'): clean_path = image_path.split('?')[0].lstrip('/') if not os.path.exists(clean_path): @@ -197,7 +211,9 @@ def scan(): "barcode": barcode_val, "name": name, "price": int(price), - "image": image_path + "image": image_path, + "stock": stock, + "unit_type": unit_type } socketio.emit('new_scan', product_data) @@ -288,6 +304,25 @@ def upload_image(): timestamp = int(time.time()) return jsonify({"status": "success", "image_url": f"/static/cache/{filename}?t={timestamp}"}), 200 +@app.route('/api/scale/weight', methods=['POST']) +def update_scale_weight(): + data = request.get_json() + + # Assuming the scale sends {"weight": 1250} (in grams) + weight_grams = data.get('weight', 0) + + # Optional: Convert to kg if you prefer + weight_kg = round(weight_grams / 1000, 3) + + # Broadcast to all connected clients via SocketIO + socketio.emit('scale_update', { + "grams": weight_grams, + "kilograms": weight_kg, + "timestamp": time.time() + }) + + return jsonify({"status": "received"}), 200 + # @app.route('/process_payment', methods=['POST']) # @login_required # def process_payment(): diff --git a/templates/index.html b/templates/index.html index 501bffa..5c710cf 100644 --- a/templates/index.html +++ b/templates/index.html @@ -252,6 +252,23 @@ [data-theme="dark"] .btn-close { filter: invert(1) grayscale(100%) brightness(200%); } + + /* Fix for the missing dropdown arrow */ + .form-select { + background-color: var(--input-bg) !important; + color: var(--text-main) !important; + border: none !important; + /* This ensures the arrow icon is still rendered over the custom background */ + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23adb5bd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e") !important; + background-repeat: no-repeat !important; + background-position: right 0.75rem center !important; + background-size: 16px 12px !important; + } + + [data-theme="dark"] .form-select { + /* Lighter arrow for dark mode */ + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dcddde' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e") !important; + } @@ -335,8 +352,22 @@ placeholder="Barcode" required> - + +
+
+ +
+
+ +
+
+ +
- - - - - Código - - - Nombre - - - Precio - + + Código + Nombre + Stock + Precio Acciones {% for p in products %} - - - - {{ p[0] }} - + + {{ p[0] }} {{ p[1] }} + {{ p[4] }} {{ p[5] }} - - @@ -442,7 +464,6 @@
- @@ -619,12 +640,25 @@ updateForm(d.barcode, d.name || '', '', d.image || '', 'Crear: ' + d.barcode); }); + socket.on('scale_update', function (data) { + console.log("Current Weight:", data.grams + "g"); + // If the unit type is 'kg', update the stock field automatically + const unitType = document.getElementById('form-unit-type').value; + if (unitType === 'kg') { + document.getElementById('form-stock').value = data.kilograms; + } + }); + // Replace your existing updateForm function with this one - function updateForm(b, n, p, i, t) { + function updateForm(b, n, p, i, t, stock, unit) { dismissPrompt(); document.getElementById('form-barcode').value = b; document.getElementById('form-name').value = n; - document.getElementById('form-price').value = (p !== undefined && p !== null) ? p : ''; + document.getElementById('form-price').value = p || ''; + document.getElementById('form-stock').value = stock || 0; + document.getElementById('form-unit-type').value = unit || 'unit'; + document.getElementById('form-image').value = i || ''; + document.getElementById('form-title').innerText = t; // Add a timestamp to the URL if it's a local cache image let displayImg = i || './static/placeholder.png'; @@ -644,25 +678,22 @@ document.getElementById('new-product-prompt').classList.add('d-none'); } - function editProduct(b, n, p, i) { + function editProduct(b, n, p, i, stock, unit) { document.getElementById('editProductName').innerText = n; - document.getElementById('editModal').dataset.barcode = b; - document.getElementById('editModal').dataset.name = n; - document.getElementById('editModal').dataset.price = p; - document.getElementById('editModal').dataset.image = i; // This captures the image + const modal = document.getElementById('editModal'); + modal.dataset.barcode = b; + modal.dataset.name = n; + modal.dataset.price = p; + modal.dataset.image = i; + modal.dataset.stock = stock; + modal.dataset.unit = unit; } function confirmEdit() { - const modal = document.getElementById('editModal'); - updateForm( - modal.dataset.barcode, - modal.dataset.name, - modal.dataset.price, - modal.dataset.image, - 'Editando: ' + modal.dataset.name - ); + const m = document.getElementById('editModal'); + updateForm(m.dataset.barcode, m.dataset.name, m.dataset.price, m.dataset.image, 'Editando: ' + m.dataset.name, m.dataset.stock, m.dataset.unit); window.scrollTo({ top: 0, behavior: 'smooth' }); - bootstrap.Modal.getInstance(modal).hide(); + bootstrap.Modal.getInstance(m).hide(); } function confirmDelete() {