Files
mesh-catalog/modules/brother/cmd/brother/scan.go
T
jschoubben d5f9cd465c
mesh/merge-gate head d5f9cd465c96 merged into trunk d62c2edc35f3 — pass: builds brother → shanks; no bus step; every machine composes with the change as it…
mesh/repo-check head d5f9cd465c96 merged into trunk d62c2edc35f3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
Send brother's scan settings on one line and scan from the feeder, both sides, into one PDF (issue 1452)
The Brother ignores the whole body when it holds a line break, so every setting was ignored and the glass was scanned at its default.
2026-10-21 01:05:55 +02:00

117 lines
4.1 KiB
Go

package main
import (
"context"
"encoding/base64"
"fmt"
"time"
)
// EventAdded is the module's event: a scan was kept in the bucket (ADR 0464, ADR 0468 clause 2), said by the device
// process. Consumers declare brother.scan.added; it
// travels as mesh.mod.brother.event.scan.added. It names the object and never carries the image.
const EventAdded = "scan.added"
// Service is what the tools do, over one reading of the settings. The device process's own scan is Capture's.
type Service struct {
Settings Settings
// Node is this machine's name, for the event.
Node string
Now func() time.Time
// Asks is how the verbs reach the device process, which alone holds a credential.
Asks AskBus
// AnswerWait is how long a verb waits for the device process's answer (ScanWait for a scan); AnswerPoll how
// often it looks.
AnswerWait, ScanWait, AnswerPoll time.Duration
}
// ScanNow asks the device process to scan, crop, build the PDF, keep it in the bucket and say scan.added, waits for
// the stored scan's key, fetches its parts through the device process, joins them, checks their SHA-256 and files
// the PDF in the scan folder (temp and rename, no overwrite). It answers the event's body and the path. Nothing is
// asked before scanner and scan-folder are known to be set (1440/AC7); the tool never reads a credential.
func (s *Service) ScanNow(ctx context.Context, source string, duplex bool) (map[string]any, error) {
if err := s.Settings.Missing(); err != nil {
return nil, err
}
if _, err := ParseSource(source, duplex); err != nil {
return nil, err
}
ans, err := s.call(ctx, Ask{Op: OpScan, Source: source, Duplex: duplex}, s.ScanWait)
if err != nil {
return nil, err
}
key, _ := ans["key"].(string)
sum, _ := ans["sha256"].(string)
parts, _ := ans["parts"].(float64)
if key == "" || sum == "" || parts < 1 || parts > MostAnswerParts {
return nil, fmt.Errorf("the device process's answer to the scan names no stored scan: %v", ans)
}
got := make([][]byte, 0, int(parts))
for i := 1; i <= int(parts); i++ {
p, err := s.fetchPart(ctx, key, i)
if err != nil {
return nil, fmt.Errorf("the scan is kept as %s, but its part %d could not be fetched: %w", key, i, err)
}
got = append(got, p)
}
pdf, err := joinParts(got, sum)
if err != nil {
return nil, fmt.Errorf("the scan is kept as %s: %w", key, err)
}
at, err := time.Parse(time.RFC3339, fmt.Sprint(ans["scanned-at"]))
if err != nil {
at = s.Now()
}
path, err := File(s.Settings.Folder, pdf, at.Local())
if err != nil {
return nil, fmt.Errorf("the scan is kept as %s, but could not be filed: %w", key, err)
}
body := map[string]any{}
for k, v := range ans {
if k != "key" && k != "size" && k != "parts" {
body[k] = v
}
}
body["path"] = path
return body, nil
}
// fetchPart is one part of a stored object, asked of the device process and decoded.
func (s *Service) fetchPart(ctx context.Context, object string, part int) ([]byte, error) {
ans, err := s.call(ctx, Ask{Op: OpDocument, Object: object, Part: part}, s.AnswerWait)
if err != nil {
return nil, err
}
data, _ := ans["data"].(string)
raw, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return nil, fmt.Errorf("the part is not base64: %w", err)
}
return raw, nil
}
// ScanStatus says whether the scanner answers, and what it is doing. A scanner that does not answer is an
// answer, not an error: that is how the module's health is read.
func (s *Service) ScanStatus(ctx context.Context) (map[string]any, error) {
if err := s.Settings.Missing(); err != nil {
return nil, err
}
st, err := NewClient(s.Settings.Scanner).Status(ctx)
if err != nil {
return map[string]any{"answers": false, "why": err.Error()}, nil
}
return map[string]any{"answers": true, "state": st.State, "adf-state": st.AdfState}, nil
}
// ScanList is the newest filed scans, newest first.
func (s *Service) ScanList(limit int) (map[string]any, error) {
if err := s.Settings.Missing(); err != nil {
return nil, err
}
scans, err := List(s.Settings.Folder, limit)
if err != nil {
return nil, err
}
return map[string]any{"folder": s.Settings.Folder, "scans": scans}, nil
}