Skip to content
This repository was archived by the owner on Jul 31, 2026. It is now read-only.

Commit f7d14f0

Browse files
feat: smart dual-tag workflow — motor-aware prefetch + simplified Case 2
Motor ON or OFF, same logic: - 1 UID detected → fetch twin from Firestore (Case 1, unchanged) - 2 UIDs physically detected → update both directly, no sync twin fetch (Case 2) Case 2 changes: - Remove 2 synchronous readInventoryDocTwinTag calls during SENDING - Remove conflict/decision-matrix check (physical co-detection = valid pair) - Always write twin_tag_uid linking A↔B (idempotent, self-repairing) - Net result: Case 2 goes from 4 HTTP calls to 2 PATCH calls Container weight fallback for Case 2: - cloudWorkerTask now prefetches container for uid_b alongside uid_a - computeWeightAvailable falls back to uid_b's container if uid_a has none (fixes wrong net weight when uid_a is the filament tag, not the masterspool) - gContainerBFetch* globals added with resets at all WF transitions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b69dc83 commit f7d14f0

1 file changed

Lines changed: 74 additions & 84 deletions

File tree

‎src/main.cpp‎

Lines changed: 74 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,27 @@
1212
// §3 FORWARD DECLARATIONS 147– 226
1313
// §4 WEIGHT ROUNDING 227– 246
1414
// §5 GLOBAL OBJECTS 247– 260
15-
// §6 CONFIGURATION VARIABLES 261– 449
16-
// §7 OLED DISPLAY 450– 567
17-
// §8 CLOUD PARSING 568– 582
18-
// §9 WIFI SETUP 583– 855
19-
// §10 LITTLEFS 856– 900
20-
// §11 FIREBASE AUTHENTICATION 901– 1094
21-
// §12 FIRESTORE SCALE HEARTBEAT & SYNC 1095– 2059
22-
// §13 WEBSOCKET 2060– 2086
23-
// §14 5 — CLOUD WORKER TASK (non-blocking Firestore on core 0) 2087– 2121
24-
// §15 5 — UNIFIED WS FRAME BUILDER 2122– 2161
25-
// §16 WEIGHT FILTER HELPERS 2162– 2176
26-
// §17 POST-SEND STATE RESET (shared by all send paths) 2177– 2195
27-
// §18 SHARED WEIGHT PUSH HANDLER (used by /api/weight and /api/push-weight) 2196– 2268
28-
// §19 WEB SERVER 2269– 2969
29-
// §20 CLOUD COMMUNICATION 2970– 3152
30-
// §21 WEIGH WORKFLOW (IDLE → SCANNING → STABLE_WAIT → SENDING) 3153– 3374
31-
// §22 mDNS 3375– 3412
32-
// §23 SCALE 3413– 3504
33-
// §24 RFID 3505– 3834
34-
// §25 OTA — Over-the-air firmware + filesystem update 3835– 4203
35-
// §26 SETUP & LOOP 4204– 4576
15+
// §6 CONFIGURATION VARIABLES 261– 454
16+
// §7 OLED DISPLAY 455– 572
17+
// §8 CLOUD PARSING 573– 587
18+
// §9 WIFI SETUP 588– 860
19+
// §10 LITTLEFS 861– 905
20+
// §11 FIREBASE AUTHENTICATION 906– 1099
21+
// §12 FIRESTORE SCALE HEARTBEAT & SYNC 1100– 2030
22+
// §13 WEBSOCKET 2031– 2057
23+
// §14 5 — CLOUD WORKER TASK (non-blocking Firestore on core 0) 2058– 2098
24+
// §15 5 — UNIFIED WS FRAME BUILDER 2099– 2138
25+
// §16 WEIGHT FILTER HELPERS 2139– 2153
26+
// §17 POST-SEND STATE RESET (shared by all send paths) 2154– 2172
27+
// §18 SHARED WEIGHT PUSH HANDLER (used by /api/weight and /api/push-weight) 2173– 2245
28+
// §19 WEB SERVER 2246– 2946
29+
// §20 CLOUD COMMUNICATION 2947– 3129
30+
// §21 WEIGH WORKFLOW (IDLE → SCANNING → STABLE_WAIT → SENDING) 3130– 3364
31+
// §22 mDNS 3365– 3402
32+
// §23 SCALE 3403– 3494
33+
// §24 RFID 3495– 3824
34+
// §25 OTA — Over-the-air firmware + filesystem update 3825– 4193
35+
// §26 SETUP & LOOP 4194– 4566
3636
//
3737
// To regenerate this block: ./scripts/update_toc.sh
3838
// ─── TOC END ───────────────────────────────────────────────
@@ -427,6 +427,11 @@ static String gContainerFetchUID = "";
427427
static String gTwinFetchUID = "";
428428
static String gTwinFetchResult = "";
429429
static volatile bool gTwinFetchDone = false;
430+
// uid_b (2nd physical tag) container prefetch — used when motor scans both tags
431+
// and uid_a happens to be the filament tag (no container_weight).
432+
static String gContainerBFetchUID = "";
433+
static float gContainerBFetchResult = 0.0f;
434+
static volatile bool gContainerBFetchDone = false;
430435

431436
static volatile bool gCloudSendPending = false;
432437
static volatile bool gCloudSendDone = false;
@@ -1593,9 +1598,15 @@ float computeWeightAvailable(float raw_grams, const String& uid_a) {
15931598
float measure_gr = 0.0f;
15941599
float container;
15951600
if (wfContainerFetched && wfContainerWeight > 0.0f) {
1596-
// Use the value prefetched during SCANNING — avoids a duplicate HTTP call
1601+
// Use the value prefetched during SCANNING (uid_a) — avoids a duplicate HTTP call
15971602
container = wfContainerWeight;
15981603
netLog("CONTAINER cache hit=" + String(container,1) + "g uid=" + uid_a);
1604+
} else if (wfContainerFetched && wfContainerWeight == 0.0f
1605+
&& gContainerBFetchDone && gContainerBFetchResult > 0.0f) {
1606+
// uid_a has no container_weight — fall back to uid_b's prefetched container.
1607+
// Happens when motor scans both tags and uid_a is the filament tag.
1608+
container = gContainerBFetchResult;
1609+
netLog("CONTAINER_B fallback=" + String(container,1) + "g uid_b=" + gContainerBFetchUID);
15991610
} else {
16001611
container = readInventoryContainerWeight(uid_a, &measure_gr);
16011612
}
@@ -1937,55 +1948,20 @@ bool updateScaleLastSpool(const String& uid_a, const String& uid_b = "", float w
19371948
}
19381949
}
19391950
} else {
1940-
// Case 2: Two different tags detected — implement decision matrix from §6.1
1941-
String twin_a = "", twin_b = "";
1942-
readInventoryDocTwinTag(uidAHex, twin_a);
1943-
readInventoryDocTwinTag(uidBHex, twin_b);
1944-
1945-
Serial.printf("[WEIGHT] 2 tags detected: uid_a=%s (twin=%s) uid_b=%s (twin=%s)\n",
1946-
uidAHex.c_str(), twin_a.length() > 0 ? twin_a.c_str() : "none",
1947-
uidBHex.c_str(), twin_b.length() > 0 ? twin_b.c_str() : "none");
1948-
1949-
// Decision matrix (§6.1):
1950-
// [A=B, B=A] OK Paired correctly -> update both
1951-
// [A=B, B=null] WARNING Asymmetric -> update both, fix B.twin_tag_uid=A
1952-
// [A=null, B=A] WARNING Asymmetric -> update both, fix A.twin_tag_uid=B
1953-
// [A=null, B=null] NEW New pair -> update both, set A.twin_tag_uid=B, B.twin_tag_uid=A
1954-
// [A=C, B=*] CONFLICT Conflict -> only update A, log warning
1955-
// [A=*, B=C] CONFLICT Conflict -> only update B, log warning
1956-
// [A!=B AND A not in {B,null} AND B not in {A,null}] -> different spools -> skip
1957-
1958-
bool twin_a_is_b = (twin_a == uidBHex);
1959-
bool twin_b_is_a = (twin_b == uidAHex);
1960-
bool twin_a_is_other = (twin_a.length() > 0 && twin_a != uidBHex);
1961-
bool twin_b_is_other = (twin_b.length() > 0 && twin_b != uidAHex);
1962-
1963-
if (twin_a_is_other) {
1964-
// A is already paired to someone else (conflict)
1965-
Serial.printf("[WEIGHT] CONFLICT: A already paired to %s, skipping weight write\n", twin_a.c_str());
1966-
return false;
1967-
}
1968-
if (twin_b_is_other) {
1969-
// B is already paired to someone else (conflict)
1970-
Serial.printf("[WEIGHT] CONFLICT: B already paired to %s, skipping weight write\n", twin_b.c_str());
1971-
return false;
1972-
}
1973-
if (!twin_a_is_b && !twin_b_is_a && twin_a.length() > 0 && twin_b.length() > 0) {
1974-
// Both have different twin_tag_uids (different spools on platform)
1975-
Serial.printf("[WEIGHT] multi-spool: uid_a links to %s, uid_b links to %s, skipping\n", twin_a.c_str(), twin_b.c_str());
1976-
return false;
1977-
}
1978-
1979-
// All other cases are valid twin-pair scenarios — update with repair
1980-
Serial.printf("[WEIGHT] twin-pair: updating uid_a=%s uid_b=%s (non-atomic)\n", uidAHex.c_str(), uidBHex.c_str());
1951+
// Case 2: Two different tags physically detected simultaneously.
1952+
// Both are on the scale together — they ARE a valid pair by definition.
1953+
// No conflict check, no sync twin fetches needed.
1954+
// Always update both docs and auto-link them as twins (idempotent repair).
1955+
Serial.printf("[WEIGHT] 2-tag pair: uid_a=%s uid_b=%s weight=%.1f\n",
1956+
uidAHex.c_str(), uidBHex.c_str(), weight_available);
1957+
netLog("2UID uid_a=" + uidAHex + " uid_b=" + uidBHex + " w=" + String(weight_available,1) + "g");
19811958

19821959
HTTPClient http;
19831960
bool success = true;
19841961

1985-
// Update A (weight + possibly twin_tag_uid repair)
1962+
// Update A — weight + twin_tag_uid link (always written so mis-linked pairs self-repair)
19861963
String docPath_a = "users/" + firebaseUid + "/inventory/" + uidAHex;
1987-
String maskStr_a = "?updateMask.fieldPaths=weight_available&updateMask.fieldPaths=last_update";
1988-
if (!twin_a_is_b) maskStr_a += "&updateMask.fieldPaths=twin_tag_uid";
1964+
String maskStr_a = "?updateMask.fieldPaths=weight_available&updateMask.fieldPaths=last_update&updateMask.fieldPaths=twin_tag_uid";
19891965
String url_a = "https://firestore.googleapis.com/v1/projects/tigertag-connect/databases/(default)/documents/" + docPath_a + maskStr_a;
19901966

19911967
if (http.begin(url_a)) {
@@ -1995,9 +1971,7 @@ bool updateScaleLastSpool(const String& uid_a, const String& uid_b = "", float w
19951971
StaticJsonDocument<256> doc_a;
19961972
doc_a["fields"]["weight_available"]["integerValue"] = String((int)roundf(weight_available));
19971973
doc_a["fields"]["last_update"]["timestampValue"] = tsStr;
1998-
if (!twin_a_is_b) {
1999-
doc_a["fields"]["twin_tag_uid"]["stringValue"] = uidBHex;
2000-
}
1974+
doc_a["fields"]["twin_tag_uid"]["stringValue"] = uidBHex;
20011975

20021976
String payload_a;
20031977
serializeJson(doc_a, payload_a);
@@ -2013,10 +1987,9 @@ bool updateScaleLastSpool(const String& uid_a, const String& uid_b = "", float w
20131987
}
20141988
}
20151989

2016-
// Update B (weight + possibly twin_tag_uid repair)
1990+
// Update B — weight + twin_tag_uid link (always written)
20171991
String docPath_b = "users/" + firebaseUid + "/inventory/" + uidBHex;
2018-
String maskStr_b = "?updateMask.fieldPaths=weight_available&updateMask.fieldPaths=last_update";
2019-
if (!twin_b_is_a) maskStr_b += "&updateMask.fieldPaths=twin_tag_uid";
1992+
String maskStr_b = "?updateMask.fieldPaths=weight_available&updateMask.fieldPaths=last_update&updateMask.fieldPaths=twin_tag_uid";
20201993
String url_b = "https://firestore.googleapis.com/v1/projects/tigertag-connect/databases/(default)/documents/" + docPath_b + maskStr_b;
20211994

20221995
if (http.begin(url_b)) {
@@ -2026,9 +1999,7 @@ bool updateScaleLastSpool(const String& uid_a, const String& uid_b = "", float w
20261999
StaticJsonDocument<256> doc_b;
20272000
doc_b["fields"]["weight_available"]["integerValue"] = String((int)roundf(weight_available));
20282001
doc_b["fields"]["last_update"]["timestampValue"] = tsStr;
2029-
if (!twin_b_is_a) {
2030-
doc_b["fields"]["twin_tag_uid"]["stringValue"] = uidAHex;
2031-
}
2002+
doc_b["fields"]["twin_tag_uid"]["stringValue"] = uidAHex;
20322003

20332004
String payload_b;
20342005
serializeJson(doc_b, payload_b);
@@ -2096,15 +2067,21 @@ static void cloudWorkerTask(void* /*param*/) {
20962067
for (;;) {
20972068
// ── Container fetch ──────────────────────────────────────────────────
20982069
if (gContainerFetchPending) {
2099-
String uid = gContainerFetchUID; // local copy (thread-safe read)
2100-
float cw = readInventoryContainerWeight(uid);
2101-
gContainerFetchResult = cw;
2102-
// Prefetch twin tag for the same UID right now — same HTTP round-trip budget,
2103-
// so pushWeightToCloud won't need a separate TWIN_GET during SENDING.
2104-
gTwinFetchUID = uid;
2070+
String uid_a = gContainerFetchUID; // local copy (thread-safe read)
2071+
float cw_a = readInventoryContainerWeight(uid_a);
2072+
gContainerFetchResult = cw_a;
2073+
// Prefetch twin tag for uid_a — avoids a TWIN_GET during SENDING (Case 1).
2074+
gTwinFetchUID = uid_a;
21052075
gTwinFetchResult = "";
2106-
readInventoryDocTwinTag(uid, gTwinFetchResult);
2107-
gTwinFetchDone = true;
2076+
readInventoryDocTwinTag(uid_a, gTwinFetchResult);
2077+
gTwinFetchDone = true;
2078+
// If a 2nd physical tag was already detected, also prefetch its container.
2079+
// This covers Case 2 where uid_a may be the filament tag (no container_weight).
2080+
if (gContainerBFetchUID.length() > 0) {
2081+
gContainerBFetchResult = readInventoryContainerWeight(gContainerBFetchUID);
2082+
gContainerBFetchDone = true;
2083+
netLog("CONTAINER_B prefetch=" + String(gContainerBFetchResult,1) + "g uid=" + gContainerBFetchUID);
2084+
}
21082085
gContainerFetchPending = false; // clear before Done for coherent state
21092086
gContainerFetchDone = true;
21102087
}
@@ -3209,6 +3186,7 @@ void handleWeighWorkflow(float w) {
32093186
sendCountdown = (int)((SPOOL_SCAN_DURATION_MS + 999) / 1000);
32103187
// Reset async worker flags for fresh session
32113188
gContainerFetchPending = false; gContainerFetchDone = false;
3189+
gContainerBFetchUID = ""; gContainerBFetchResult = 0.0f; gContainerBFetchDone = false;
32123190
gCloudSendPending = false; gCloudSendDone = false;
32133191
netLog("WF IDLE→SCANNING slope=" + String(wfCurrentSlope,1));
32143192
}
@@ -3227,6 +3205,7 @@ void handleWeighWorkflow(float w) {
32273205
resetSlopeBuffer();
32283206
// Abandon any in-flight async requests
32293207
gContainerFetchPending = false; gContainerFetchDone = false;
3208+
gContainerBFetchUID = ""; gContainerBFetchResult = 0.0f; gContainerBFetchDone = false;
32303209
gCloudSendPending = false; gCloudSendDone = false;
32313210
netLog("WF →IDLE (retrait: slope=" + String(wfCurrentSlope,1) + " w=" + String(w,1) + ")");
32323211
return;
@@ -3238,12 +3217,22 @@ void handleWeighWorkflow(float w) {
32383217
uint32_t scanDuration = (servoEnabled && hwMotorConnected)
32393218
? SPOOL_SCAN_DURATION_MS : NO_MOTOR_UID_WAIT_MS;
32403219

3241-
// Trigger async container fetch as soon as we have the first UID
3220+
// Trigger async container fetch as soon as we have the first UID.
3221+
// Also record uid_b at that moment — cloudWorkerTask will prefetch its
3222+
// container too so Case 2 can use the right tare weight.
32423223
if (!wfContainerFetched && !gContainerFetchPending && !gContainerFetchDone
32433224
&& lastUID.length() > 0) {
32443225
gContainerFetchUID = lastUID;
3226+
gContainerBFetchUID = lastUID2; // may be empty — that's fine
3227+
gContainerBFetchDone = false;
3228+
gContainerBFetchResult = 0.0f;
32453229
gContainerFetchPending = true; // hand off to cloudWorkerTask
32463230
}
3231+
// If uid_b arrives after the prefetch was already kicked off, update the slot
3232+
// so the worker can still fetch it (only if it hasn't started the B fetch yet).
3233+
if (gContainerFetchPending && gContainerBFetchUID.length() == 0 && lastUID2.length() > 0) {
3234+
gContainerBFetchUID = lastUID2;
3235+
}
32473236
// Pick up result when the worker task is done (non-blocking)
32483237
if (gContainerFetchDone) {
32493238
wfContainerWeight = gContainerFetchResult;
@@ -3364,6 +3353,7 @@ void handleWeighWorkflow(float w) {
33643353
rfidLockedForCurrentLoad = false;
33653354
resetSlopeBuffer();
33663355
gContainerFetchPending = false; gContainerFetchDone = false;
3356+
gContainerBFetchUID = ""; gContainerBFetchResult = 0.0f; gContainerBFetchDone = false;
33673357
gCloudSendPending = false; gCloudSendDone = false;
33683358
sendPhase = ""; sendCountdown = -1;
33693359
netLog("WF DONE→IDLE (spool retiré, prêt)");

0 commit comments

Comments
 (0)