esp attempt, stock + unit type

This commit is contained in:
shironeko
2026-03-07 19:21:14 -03:00
parent 788b67804e
commit 2f2998b0fd
9 changed files with 283 additions and 53 deletions

5
SekiScaleESP/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

10
SekiScaleESP/.vscode/extensions.json vendored Normal file
View File

@@ -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"
]
}

View File

@@ -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

46
SekiScaleESP/lib/README Normal file
View File

@@ -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 <Foo.h>
#include <Bar.h>
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

View File

@@ -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

39
SekiScaleESP/src/main.cpp Normal file
View File

@@ -0,0 +1,39 @@
#include <Arduino.h>
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;
}

11
SekiScaleESP/test/README Normal file
View File

@@ -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

59
app.py
View File

@@ -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():

View File

@@ -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;
}
</style>
</head>
@@ -335,8 +352,22 @@
placeholder="Barcode" required>
<input class="form-control mb-2" type="text" name="name" id="form-name" placeholder="Nombre"
required>
<input class="form-control mb-2" type="number" name="price" id="form-price"
placeholder="Precio (CLP)" required>
<div class="row g-2 mb-2">
<div class="col-8">
<input class="form-control" type="number" name="price" id="form-price"
placeholder="Precio (CLP)" required>
</div>
<div class="col-4">
<select class="form-select" name="unit_type" id="form-unit-type">
<option value="unit">Unidad</option>
<option value="kg">Kg</option>
</select>
</div>
</div>
<input class="form-control mb-2" type="number" step="1" name="stock" id="form-stock"
placeholder="Stock Inicial">
<div class="input-group mb-3">
<input class="form-control" type="text" name="image_url" id="form-image"
@@ -398,36 +429,27 @@
<table class="table table-borderless mb-0" id="inventoryTable">
<thead>
<tr>
<th style="width:36px;">
<input class="form-check-input" type="checkbox" id="select-all"
onclick="toggleAll(this)">
</th>
<th class="col-barcode" onclick="sortTable(1)" style="cursor:pointer;">
Código <i class="bi bi-arrow-down-up ms-1" style="font-size: 0.6rem;"></i>
</th>
<th onclick="sortTable(2)" style="cursor:pointer;">
Nombre <i class="bi bi-arrow-down-up ms-1" style="font-size: 0.6rem;"></i>
</th>
<th onclick="sortTable(3)" style="cursor:pointer;">
Precio <i class="bi bi-arrow-down-up ms-1" style="font-size: 0.6rem;"></i>
</th>
<th style="width:36px;"><input class="form-check-input" type="checkbox"
id="select-all" onclick="toggleAll(this)"></th>
<th class="col-barcode" onclick="sortTable(1)">Código</th>
<th onclick="sortTable(2)">Nombre</th>
<th onclick="sortTable(3)">Stock</th>
<th onclick="sortTable(4)">Precio</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{% for p in products %}
<tr data-barcode="{{ p[0] }}">
<td>
<input class="form-check-input product-checkbox" type="checkbox"
onclick="updateBulkBar()">
</td>
<td class="col-barcode" style="font-family:monospace; font-size:.8rem;">{{ p[0] }}
</td>
<td><input class="form-check-input product-checkbox" type="checkbox"
onclick="updateBulkBar()"></td>
<td class="col-barcode">{{ p[0] }}</td>
<td class="name-cell">{{ p[1] }}</td>
<td>{{ p[4] }} <small class="text-muted">{{ p[5] }}</small></td>
<td class="price-cell" data-value="{{ p[2] }}"></td>
<td style="white-space:nowrap;">
<button class="btn btn-accent btn-sm btn-edit-sm me-1"
onclick="editProduct('{{ p[0] }}', '{{ p[1] }}', '{{ p[2] }}', '{{ p[3] }}')"
<td>
<button class="btn btn-accent btn-sm"
onclick="editProduct('{{ p[0] }}', '{{ p[1] }}', '{{ p[2] }}', '{{ p[3] }}', '{{ p[4] }}', '{{ p[5] }}')"
data-bs-toggle="modal" data-bs-target="#editModal">
<i class="bi bi-pencil"></i>
</button>
@@ -442,7 +464,6 @@
</tbody>
</table>
</div>
</div>
</div>
</div><!-- /row -->
@@ -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() {