read spool weight from edit dialog and write measured total back when it differs
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"image/png"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -27,16 +28,18 @@ import (
|
||||
)
|
||||
|
||||
type result struct {
|
||||
Image string `json:"image"`
|
||||
SpoolID string `json:"spool_id,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Weight *float64 `json:"weight,omitempty"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Confidence *float64 `json:"confidence,omitempty"`
|
||||
Weights []float64 `json:"weights,omitempty"`
|
||||
ModelConfidences []string `json:"model_confidences,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Image string `json:"image"`
|
||||
SpoolID string `json:"spool_id,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
SiteRemainingGrams *float64 `json:"site_remaining_grams,omitempty"`
|
||||
Weight *float64 `json:"weight,omitempty"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Confidence *float64 `json:"confidence,omitempty"`
|
||||
Weights []float64 `json:"weights,omitempty"`
|
||||
ModelConfidences []string `json:"model_confidences,omitempty"`
|
||||
Updated bool `json:"updated,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// spoolURLPrefix is the only QR payload we accept. Anything else (a different
|
||||
@@ -46,8 +49,9 @@ const spoolURLPrefix = "https://spooldb.com/s/"
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
verbose := flag.Bool("v", false, "print the auth mechanism being used to stderr")
|
||||
dryRun := flag.Bool("n", false, "dry run: read and report, but do not write weight changes back to the site")
|
||||
flag.Usage = func() {
|
||||
log.Printf("usage: %s [-v] <image>...", os.Args[0])
|
||||
log.Printf("usage: %s [-v] [-n] <image>...", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
@@ -64,12 +68,12 @@ func main() {
|
||||
log.Printf("auth: %s", auth.name)
|
||||
}
|
||||
|
||||
locator := &spoolLocator{}
|
||||
defer locator.close()
|
||||
sp := &spoolSync{}
|
||||
defer sp.close()
|
||||
|
||||
results := make([]result, 0, flag.NArg())
|
||||
for _, path := range flag.Args() {
|
||||
results = append(results, processImage(path, auth, locator))
|
||||
results = append(results, processImage(path, auth, sp, *dryRun))
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
@@ -77,17 +81,17 @@ func main() {
|
||||
enc.Encode(results)
|
||||
}
|
||||
|
||||
// spoolLocator lazily logs into spooldb on first use, so its (slow) browser
|
||||
// login overlaps with the first image's weight read rather than blocking it.
|
||||
// Location lookup is best-effort: any failure is logged and the location is
|
||||
// simply omitted from the output.
|
||||
type spoolLocator struct {
|
||||
// spoolSync lazily logs into spooldb on first use, so its (slow) browser login
|
||||
// overlaps with the first image's weight read rather than blocking it. Every
|
||||
// site interaction is best-effort: failures are logged and the corresponding
|
||||
// output fields are simply omitted.
|
||||
type spoolSync struct {
|
||||
once sync.Once
|
||||
client *spooldb.Client
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *spoolLocator) login() (*spooldb.Client, error) {
|
||||
func (s *spoolSync) login() (*spooldb.Client, error) {
|
||||
s.once.Do(func() {
|
||||
user, pass := os.Getenv("SPOOLDB_USER"), os.Getenv("SPOOLDB_PASS")
|
||||
if user == "" || pass == "" {
|
||||
@@ -111,24 +115,40 @@ func (s *spoolLocator) login() (*spooldb.Client, error) {
|
||||
return s.client, s.err
|
||||
}
|
||||
|
||||
// location returns the spool's storage location, or "" if it can't be looked up.
|
||||
func (s *spoolLocator) location(spoolID string) string {
|
||||
// info reads the spool's current location and weights, or nil if unavailable.
|
||||
func (s *spoolSync) info(spoolID string) *spooldb.SpoolInfo {
|
||||
client, err := s.login()
|
||||
if err != nil {
|
||||
log.Printf("spooldb: skipping location lookup: %v", err)
|
||||
return ""
|
||||
log.Printf("spooldb: skipping spool lookup: %v", err)
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
loc, err := client.SpoolLocation(ctx, spoolID)
|
||||
info, err := client.SpoolInfo(ctx, spoolID)
|
||||
if err != nil {
|
||||
log.Printf("spooldb: location lookup for %s: %v", spoolID, err)
|
||||
return ""
|
||||
log.Printf("spooldb: lookup for %s: %v", spoolID, err)
|
||||
return nil
|
||||
}
|
||||
return loc
|
||||
return &info
|
||||
}
|
||||
|
||||
func (s *spoolLocator) close() {
|
||||
// setTotal writes a new total (with-spool) weight back to the site.
|
||||
func (s *spoolSync) setTotal(spoolID string, grams float64) bool {
|
||||
client, err := s.login()
|
||||
if err != nil {
|
||||
log.Printf("spooldb: skipping weight update: %v", err)
|
||||
return false
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
if err := client.SetTotalWeight(ctx, spoolID, grams); err != nil {
|
||||
log.Printf("spooldb: weight update for %s: %v", spoolID, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *spoolSync) close() {
|
||||
if s.client != nil {
|
||||
s.client.Close()
|
||||
}
|
||||
@@ -136,7 +156,7 @@ func (s *spoolLocator) close() {
|
||||
|
||||
// processImage reads one photo, capturing any failure in the result's Error
|
||||
// field so a single bad image doesn't abort the whole batch.
|
||||
func processImage(path string, auth authInfo, locator *spoolLocator) result {
|
||||
func processImage(path string, auth authInfo, sp *spoolSync, dryRun bool) result {
|
||||
r := result{Image: path}
|
||||
|
||||
img, err := loadImage(path)
|
||||
@@ -157,18 +177,23 @@ func processImage(path string, auth authInfo, locator *spoolLocator) result {
|
||||
r.SpoolID = spoolID(url)
|
||||
r.URL = url
|
||||
|
||||
// Look up the spool's location concurrently with the (slower) weight read.
|
||||
var loc string
|
||||
// Read the spool's current location/weight from the site concurrently with
|
||||
// the (slower) weight read off the photo.
|
||||
var info *spooldb.SpoolInfo
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
loc = locator.location(r.SpoolID)
|
||||
info = sp.info(r.SpoolID)
|
||||
}()
|
||||
|
||||
w, err := readWeight(img, auth)
|
||||
wg.Wait()
|
||||
r.Location = loc
|
||||
if info != nil {
|
||||
r.Location = info.Location
|
||||
rem := info.RemainingGrams
|
||||
r.SiteRemainingGrams = &rem
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
r.Error = fmt.Sprintf("read weight: %v", err)
|
||||
@@ -183,6 +208,21 @@ func processImage(path string, auth authInfo, locator *spoolLocator) result {
|
||||
r.Confidence = &w.confidence
|
||||
r.Weights = w.weights
|
||||
r.ModelConfidences = w.modelConfidences
|
||||
|
||||
// The photo gives total weight (filament + spool); the site stores remaining
|
||||
// filament. Write the measured total back when it differs from the site's
|
||||
// total — the site recomputes remaining using the empty-spool weight.
|
||||
if info != nil && math.Abs(w.weight-info.TotalGrams) >= 1 {
|
||||
newRemaining := w.weight - info.EmptySpoolGrams
|
||||
if dryRun {
|
||||
log.Printf("spooldb: %s would update remaining %.0fg -> %.0fg (measured total %.0fg)",
|
||||
r.SpoolID, info.RemainingGrams, newRemaining, w.weight)
|
||||
} else if sp.setTotal(r.SpoolID, w.weight) {
|
||||
r.Updated = true
|
||||
log.Printf("spooldb: %s updated remaining %.0fg -> %.0fg (measured total %.0fg)",
|
||||
r.SpoolID, info.RemainingGrams, newRemaining, w.weight)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user