250 lines
9.5 KiB
Go
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)
|
|
},
|
|
},
|
|
}
|
|
}
|