esp attempt, stock + unit type
This commit is contained in:
5
SekiScaleESP/.gitignore
vendored
Normal file
5
SekiScaleESP/.gitignore
vendored
Normal 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
10
SekiScaleESP/.vscode/extensions.json
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
37
SekiScaleESP/include/README
Normal file
37
SekiScaleESP/include/README
Normal 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
46
SekiScaleESP/lib/README
Normal 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
|
||||
16
SekiScaleESP/platformio.ini
Normal file
16
SekiScaleESP/platformio.ini
Normal 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
39
SekiScaleESP/src/main.cpp
Normal 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
11
SekiScaleESP/test/README
Normal 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
59
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():
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user