Files
mesh-catalog/modules/brother/cmd/brother/main.go
T

250 lines
9.5 KiB
Go

package main
import (
"context"
"fmt"
"os"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
func main() {
if len(os.Args) > 1 && os.Args[1] == "device" {
os.Exit(device())
}
if err := stdio.Serve("", tools(os.Getenv("BROTHER_SETTINGS"), time.Now)); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// intArg reads a whole number out of a tool's arguments: a JSON number, which arrives as a float64.
func intArg(args map[string]any, name string) (int, error) {
v, ok := args[name]
if !ok {
return 0, nil
}
switch n := v.(type) {
case float64:
if n != float64(int(n)) {
return 0, fmt.Errorf("%s is a whole number", name)
}
return int(n), nil
case int:
return n, nil
}
return 0, fmt.Errorf("%s is a whole number", name)
}
func strArg(args map[string]any, name string) (string, error) {
v, ok := args[name]
if !ok {
return "", nil
}
s, isStr := v.(string)
if !isStr {
return "", fmt.Errorf("%s is a string", name)
}
return s, nil
}
// askFrom reads the arguments an ask takes out of a tool's.
func askFrom(args map[string]any) (Ask, error) {
var a Ask
var err error
for name, dst := range map[string]*string{"page": &a.Page, "object": &a.Object, "upload": &a.Upload, "sha256": &a.SHA256,
"data": &a.Data, "sides": &a.Sides, "media": &a.Media} {
if *dst, err = strArg(args, name); err != nil {
return a, err
}
}
for name, dst := range map[string]*int{"part": &a.Part, "parts": &a.Parts, "copies": &a.Copies} {
if *dst, err = intArg(args, name); err != nil {
return a, err
}
}
return a, nil
}
// tools are the scanner seat's and the printer seat's verbs, served as <seat>.<verb> (ADR 0467 clause 2, ADR 0468).
// The settings are read at each call, so a changed config value is the next call's. None opens a credential: what
// needs one is asked of the device process.
func tools(settingsFile string, now func() time.Time) []stdio.Tool {
service := func() (*Service, error) {
s, err := LoadSettings(settingsFile)
if err != nil {
return nil, err
}
return &Service{Settings: s, Node: os.Getenv("MESH_NODE"), Now: now, Asks: askBus,
AnswerWait: answerWait, ScanWait: scanWait, AnswerPoll: answerPoll}, nil
}
// ask is a tool that reads its arguments, and answers what the service gives.
ask := func(run func(s *Service, ctx context.Context, a Ask) (map[string]any, error), bound time.Duration) func(map[string]any) (any, error) {
return func(args map[string]any) (any, error) {
a, err := askFrom(args)
if err != nil {
return nil, err
}
s, err := service()
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), bound)
defer cancel()
return run(s, ctx, a)
}
}
str := func(d string) map[string]any { return map[string]any{"type": "string", "description": d} }
num := func(d string) map[string]any { return map[string]any{"type": "integer", "description": d} }
return []stdio.Tool{
{
Name: "printer.upload",
Description: "Send one part of a document to the device process, to be printed later (printer.print with the upload id). " +
"Cut the document into parts of at most 700 KiB (700 KiB is the limit), number them from 1, and send each with the " +
"whole document's SHA-256. When the last part has come and the joined SHA-256 matches, the document is stored as " +
"uploads/<upload>.pdf or .jpg and the answer says complete; a wrong SHA-256 stores nothing. PDF and JPEG only.",
Input: map[string]any{"type": "object", "required": []string{"upload", "part", "parts", "sha256", "data"}, "properties": map[string]any{
"upload": str("an id of your choosing: letters, digits, - and _, at most 64"),
"part": num("this part's number, from 1"),
"parts": num("how many parts the document has"),
"sha256": str("the whole document's SHA-256, lower-case hex"),
"data": str("this part's bytes, base64"),
}},
Run: ask(func(s *Service, ctx context.Context, a Ask) (map[string]any, error) { return s.PrinterUpload(ctx, a) }, 2*time.Minute),
},
{
Name: "printer.print",
Description: "Print a document on the printer: a scan kept in the bucket (object) or a completed upload (upload). The device " +
"process sends it as an IPP Print-Job in its own format (PDF or JPEG) and answers the job's id and state.",
Input: map[string]any{"type": "object", "properties": map[string]any{
"object": str("a scan's key, as scan.added names it"),
"upload": str("a completed upload's id"),
"copies": num("1 to 99 (default 1)"),
"sides": str("one-sided (default), two-sided-long-edge or two-sided-short-edge"),
"media": str("the paper's size name (default " + DefaultMedia + ")"),
}},
Run: ask(func(s *Service, ctx context.Context, a Ask) (map[string]any, error) { return s.PrinterPrint(ctx, a) }, 2*time.Minute),
},
{
Name: "scanner.document",
Description: "One part of a scan kept in the bucket, by the key scan.added names (object): answers part, parts (how many), " +
"size, sha256 (of the whole document) and data (this part, base64). Join the parts and check the SHA-256.",
Input: map[string]any{"type": "object", "required": []string{"object", "part"}, "properties": map[string]any{
"object": str("the scan's key"),
"part": num("the part's number, from 1"),
}},
Run: ask(func(s *Service, ctx context.Context, a Ask) (map[string]any, error) {
return s.ScanDocument(ctx, a.Object, a.Part)
}, 2*time.Minute),
},
{
Name: "printer.status",
Description: "The printer's state without a login: over IPP its state (idle, processing or stopped), its state " +
"reasons (such as toner-low-warning), its toner and other markers with their levels in percent (below 0 " +
"is unknown), and over eSCL the scanner's state and its document feeder's. A part that does not answer " +
"is said as answers false with why. Read-only.",
Run: func(map[string]any) (any, error) {
s, err := service()
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return s.PrinterStatus(ctx)
},
},
{
Name: "printer.settings",
Description: "Read one page of the printer's admin site, read-only: the module's device process logs in with the " +
"secret admin-password (the operator's; it trusts the printer's pinned certificate alone), gets the page and logs " +
"out; this tool asks it and waits up to a minute for its answer. Answers the " +
"page's title, its links (href and text) and its forms with their fields (name, type, value, label); a " +
"password field's value is never given. It sends nothing but the login and the logout, and writes no " +
"setting of the printer.",
Input: map[string]any{
"type": "object",
"properties": map[string]any{
"page": map[string]any{"type": "string", "description": "a page of the admin site: a path starting with / and ending in .html (default /general/status.html)"},
},
},
Run: func(args map[string]any) (any, error) {
page := ""
if v, ok := args["page"]; ok {
str, isStr := v.(string)
if !isStr {
return nil, fmt.Errorf("page is a string")
}
page = str
}
s, err := service()
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
return s.PrinterSettings(ctx, page)
},
},
{
Name: "scanner.scan",
Description: "Scan the glass of the configured network scanner: the device process cuts the document out of the platen's " +
"white, keeps it as a one-page PDF in the module's bucket under its sha256 and says scan.added on the bus; this " +
"tool then fetches it and files it in the scan folder. Waits up to three minutes. Answers the event's body: " +
"the object's key, its size and sha256, its real size in millimetres, the resolution the scanner really gave, " +
"whether it was cropped, and the file's path. The image itself is never sent.",
Run: func(map[string]any) (any, error) {
s, err := service()
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), scanWait+10*time.Minute)
defer cancel()
return s.ScanNow(ctx)
},
},
{
Name: "scanner.status",
Description: "Whether the configured scanner answers, and what it is doing: its state (Idle or Processing) " +
"and its document feeder's state. A scanner that does not answer is said as answers false with why. Read-only.",
Run: func(map[string]any) (any, error) {
s, err := service()
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
return s.ScanStatus(ctx)
},
},
{
Name: "scanner.list",
Description: "The newest scans filed in the scan folder, newest first, with their size and time. Read-only.",
Input: map[string]any{
"type": "object",
"properties": map[string]any{
"limit": map[string]any{"type": "integer", "description": "at most this many scans (default 20)"},
},
},
Run: func(args map[string]any) (any, error) {
limit := 20
if v, ok := args["limit"]; ok {
f, isNum := v.(float64)
if i, isInt := v.(int); isInt {
f, isNum = float64(i), true
}
if !isNum || f < 1 || f > 1000 || f != float64(int(f)) {
return nil, fmt.Errorf("limit is a whole number from 1 to 1000")
}
limit = int(f)
}
s, err := service()
if err != nil {
return nil, err
}
return s.ScanList(limit)
},
},
}
}