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
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.
117 lines
4.1 KiB
Go
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
|
|
}
|