DVX_GUI/src/libs/kpunch/print/printJob.c
2026-10-13 13:49:12 -05:00

545 lines
19 KiB
C

// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// printJob.c -- Printing library: settings, printer ports and jobs
// (see print.h)
//
// A byte goes to a port only when a status check says the printer is
// ready, so the BIOS never sits in its own busy wait: while the printer
// is busy the job calls the idle hook and checks again, and a printer
// that stays busy for PRINT_BUSY_TIMEOUT_MS is reported like any other
// problem. A problem is put to the ask hook; Retry tries the same byte
// again, Cancel ends the job.
#include "printInt.h"
#include "dvxPlat.h"
#include "taskSwch.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
DVX_ABI_STAMP(print);
#define PRINT_SECTION "printer"
#define INI_PORT "port"
#define INI_FILE "file"
#define INI_MODEL "model"
#define INI_LINES "linesPerPage"
#define INI_COLUMNS "columns"
#define INI_MARGIN_TOP "marginTop"
#define INI_MARGIN_BOTTOM "marginBottom"
#define INI_MARGIN_LEFT "marginLeft"
#define INI_MARGIN_RIGHT "marginRight"
#define INI_TAB "tabWidth"
#define INI_WRAP "wrap"
#define INI_HEADER "header"
#define INI_FOOTER "footer"
// Bytes written to a file between two calls of the idle hook
#define PRINT_FILE_YIELD_BYTES 4096
struct PrintJobS {
PrintHooksT hooks;
FILE *fp; // file destination, else NULL
int32_t port; // port destination
uint32_t bytes;
int32_t sinceIdle; // bytes since the idle hook ran
int32_t idleEvery;
bool cancelled;
bool idled; // the idle hook ran during this write
char where[PRINT_WHERE_MAX];
};
const PrefsChoiceT printDestChoices[PrintDestCountE] = {
{ "LPT1", "LPT1", PrintDestLpt1E },
{ "LPT2", "LPT2", PrintDestLpt2E },
{ "LPT3", "LPT3", PrintDestLpt3E },
{ "File", "FILE", PrintDestFileE }
};
const PrefsChoiceT printModelChoices[PrintModelCountE] = {
{ "Text only (any printer)", "text", PrintModelTextE },
{ "Epson 9-pin (ESC/P)", "epson9", PrintModelEpson9E },
{ "Epson 24-pin (ESC/P)", "epson24", PrintModelEpson24E },
{ "HP LaserJet (PCL)", "laserjet", PrintModelLaserJetE }
};
static const char sNotResponding[] = "is not responding. Check that it is switched on and connected, then choose Retry.";
static const PrintPortOpsT *sOps = &printSysPortOps;
// ============================================================
// Prototypes
// ============================================================
static bool askRetry(PrintJobT *job, const char *message);
static bool idle(PrintJobT *job);
const char *printDestLabel(PrintDestE dest);
void printDestName(PrintDestE dest, const char *filePath, char *out, int32_t outSize);
const char *printDeviceProblem(PrintDestE dest, const char *filePath);
void printDeviceStore(PrefsHandleT *h, PrintModelE model, PrintDestE dest, const char *filePath);
uint32_t printJobBytes(const PrintJobT *job);
bool printJobCancelled(const PrintJobT *job);
bool printJobClose(PrintJobT *job);
PrintJobT *printJobOpen(PrintDestE dest, const char *filePath, const PrintHooksT *hooks, char *err, int32_t errSize);
bool printJobPuts(PrintJobT *job, const char *text);
bool printJobWrite(PrintJobT *job, const void *data, int32_t len);
const char *printModelLabel(PrintModelE model);
int32_t printPageLines(const PrintPageSetupT *ps, PrintModelE model);
const char *printPageSetupProblem(const PrintPageSetupT *ps);
bool printPortPresent(int32_t port);
uint8_t printPortStatus(int32_t port);
void printSetPortOps(const PrintPortOpsT *ops);
void printSettingsDefaults(PrintSettingsT *s);
void printSettingsLoad(PrintSettingsT *s);
void printSettingsRead(PrefsHandleT *h, PrintSettingsT *s);
bool printSettingsSave(const PrintSettingsT *s);
void printSettingsWrite(PrefsHandleT *h, const PrintSettingsT *s);
bool printStatusProblem(uint8_t status, bool afterWrite, const char *where, char *out, int32_t outSize);
static bool sendByte(PrintJobT *job, uint8_t byte);
static bool writeFile(PrintJobT *job, const uint8_t *data, int32_t len);
// Put a problem to the user. False (job cancelled) on Cancel or when
// there is nobody to ask.
static bool askRetry(PrintJobT *job, const char *message) {
dvxLog("Print: %s", message);
if (job->hooks.ask && job->hooks.ask(job->hooks.ctx, message) == PrintAnswerRetryE) {
return true;
}
job->cancelled = true;
return false;
}
// Let other programs run. False (job cancelled) when the hook says so.
static bool idle(PrintJobT *job) {
job->idled = true;
job->sinceIdle = 0;
if (!job->hooks.idle) {
tsYield();
return true;
}
if (!job->hooks.idle(job->hooks.ctx)) {
job->cancelled = true;
return false;
}
return true;
}
const char *printDestLabel(PrintDestE dest) {
return printDestChoices[dest].label;
}
void printDestName(PrintDestE dest, const char *filePath, char *out, int32_t outSize) {
if (dest == PrintDestFileE) {
snprintf(out, outSize, "%s", platformPathBaseName(filePath ? filePath : ""));
} else {
snprintf(out, outSize, "LPT%d", (int)(dest + PRINT_PORT_NUMBER_BASE));
}
}
// NULL when the printer can be used as chosen, else why not.
const char *printDeviceProblem(PrintDestE dest, const char *filePath) {
if (dest == PrintDestFileE && (!filePath || !filePath[0])) {
return "Type the name of the file to print to, or connect the printer to a port.";
}
return NULL;
}
// The page setup comes from DVX.INI as it is now (the Page Setup dialog
// saves it there at once), so a handle read earlier does not put back an
// older one.
void printDeviceStore(PrefsHandleT *h, PrintModelE model, PrintDestE dest, const char *filePath) {
PrintSettingsT s;
printSettingsLoad(&s);
s.model = model;
s.dest = dest;
snprintf(s.filePath, sizeof(s.filePath), "%s", filePath ? filePath : "");
printSettingsWrite(h, &s);
}
uint32_t printJobBytes(const PrintJobT *job) {
return job ? job->bytes : 0;
}
bool printJobCancelled(const PrintJobT *job) {
return !job || job->cancelled;
}
bool printJobClose(PrintJobT *job) {
if (!job) {
return false;
}
bool ok = !job->cancelled;
if (job->fp && fclose(job->fp) != 0) {
ok = false;
}
if (job->hooks.finished) {
job->hooks.finished(job->hooks.ctx, true);
}
dvxLog("Print: %s %lu bytes to %s", ok ? "finished," : "stopped after", (unsigned long)job->bytes, job->where);
free(job);
return ok;
}
PrintJobT *printJobOpen(PrintDestE dest, const char *filePath, const PrintHooksT *hooks, char *err, int32_t errSize) {
PrintJobT *job = (PrintJobT *)calloc(1, sizeof(PrintJobT));
if (!job) {
snprintf(err, errSize, "Not enough memory to print.");
return NULL;
}
if (hooks) {
job->hooks = *hooks;
}
printDestName(dest, filePath, job->where, sizeof(job->where));
if (dest == PrintDestFileE) {
job->fp = (filePath && filePath[0]) ? fopen(filePath, "wb") : NULL;
job->idleEvery = PRINT_FILE_YIELD_BYTES;
if (!job->fp) {
snprintf(err, errSize, "Cannot create the file %s.", (filePath && filePath[0]) ? filePath : "(no name)");
free(job);
return NULL;
}
} else {
job->port = (int32_t)dest;
job->idleEvery = PRINT_YIELD_BYTES;
if ((int32_t)dest < 0 || (int32_t)dest >= PRINT_PORT_COUNT || !printPortPresent(job->port)) {
snprintf(err, errSize, "This computer has no printer port %s.", job->where);
free(job);
return NULL;
}
}
return job;
}
bool printJobPuts(PrintJobT *job, const char *text) {
return printJobWrite(job, text, (int32_t)strlen(text));
}
bool printJobWrite(PrintJobT *job, const void *data, int32_t len) {
const uint8_t *p = (const uint8_t *)data;
bool ok = true;
if (!job || job->cancelled) {
return false;
}
if (job->fp) {
ok = writeFile(job, p, len);
} else {
for (int32_t i = 0; ok && i < len; i++) {
ok = sendByte(job, p[i]);
if (ok && ++job->sinceIdle >= job->idleEvery) {
ok = idle(job);
}
}
}
if (job->idled && job->hooks.finished) {
job->hooks.finished(job->hooks.ctx, false);
}
job->idled = false;
return ok;
}
const char *printModelLabel(PrintModelE model) {
return printModelChoices[model].label;
}
int32_t printPageLines(const PrintPageSetupT *ps, PrintModelE model) {
const PrintDriverT *drv = printDriver(model);
if (ps->linesPerPage > 0) {
return ps->linesPerPage;
}
return drv ? drv->linesPerPage : PRINT_LINES_STANDARD;
}
// The reason is formatted into a static buffer (valid until the next call).
const char *printPageSetupProblem(const PrintPageSetupT *ps) {
static char msg[PRINT_MSG_MAX];
// With the printer's usual page length the shorter laser page counts
int32_t lines = ps->linesPerPage ? ps->linesPerPage : PRINT_LINES_LASER;
if (ps->linesPerPage != 0 && (ps->linesPerPage < PRINT_LINES_MIN || ps->linesPerPage > PRINT_LINES_MAX)) {
snprintf(msg, sizeof(msg), "Lines per page must be 0 (the printer's usual length) or from %d to %d.", PRINT_LINES_MIN, PRINT_LINES_MAX);
} else if (ps->columns < PRINT_COLUMNS_MIN || ps->columns > PRINT_COLUMNS_MAX) {
snprintf(msg, sizeof(msg), "Characters per line must be from %d to %d.", PRINT_COLUMNS_MIN, PRINT_COLUMNS_MAX);
} else if (ps->marginTop < 0 || ps->marginBottom < 0 || ps->marginLeft < 0 || ps->marginRight < 0) {
snprintf(msg, sizeof(msg), "Margins cannot be negative.");
} else if (ps->tabWidth < 1 || ps->tabWidth > PRINT_TAB_MAX) {
snprintf(msg, sizeof(msg), "Tab width must be from 1 to %d.", PRINT_TAB_MAX);
} else if (lines - ps->marginTop - ps->marginBottom < PRINT_BODY_MIN_LINES) {
snprintf(msg, sizeof(msg), "The top and bottom margins leave no room for text.");
} else if (ps->columns - ps->marginLeft - ps->marginRight < PRINT_BODY_MIN_COLUMNS) {
snprintf(msg, sizeof(msg), "The left and right margins leave less than %d characters for text.", PRINT_BODY_MIN_COLUMNS);
} else {
return NULL;
}
return msg;
}
bool printPortPresent(int32_t port) {
return port >= 0 && port < PRINT_PORT_COUNT && sOps->present(sOps->ctx, port);
}
uint8_t printPortStatus(int32_t port) {
return sOps->status(sOps->ctx, port);
}
void printSetPortOps(const PrintPortOpsT *ops) {
sOps = ops ? ops : &printSysPortOps;
}
void printSettingsDefaults(PrintSettingsT *s) {
memset(s, 0, sizeof(*s));
s->dest = PrintDestLpt1E;
s->model = PrintModelTextE;
s->page.columns = PRINT_COLUMNS_DEFAULT;
s->page.marginTop = PRINT_MARGIN_TB_DEFAULT;
s->page.marginBottom = PRINT_MARGIN_TB_DEFAULT;
s->page.marginLeft = PRINT_MARGIN_LR_DEFAULT;
s->page.marginRight = PRINT_MARGIN_LR_DEFAULT;
s->page.tabWidth = PRINT_TAB_DEFAULT;
s->page.wrap = true;
snprintf(s->filePath, sizeof(s->filePath), "%s", PRINT_FILE_DEFAULT);
snprintf(s->page.header, sizeof(s->page.header), "%s", PRINT_HEADER_DEFAULT);
snprintf(s->page.footer, sizeof(s->page.footer), "%s", PRINT_FOOTER_DEFAULT);
}
void printSettingsLoad(PrintSettingsT *s) {
PrefsHandleT *h = prefsLoad(DVX_INI_PATH);
printSettingsRead(h, s);
prefsClose(h);
}
// A page setup that cannot be used falls back to the defaults as a whole.
void printSettingsRead(PrefsHandleT *h, PrintSettingsT *s) {
PrintSettingsT def;
printSettingsDefaults(&def);
*s = def;
if (!h) {
return;
}
s->dest = (PrintDestE)prefsGetChoice(h, PRINT_SECTION, INI_PORT, printDestChoices, PrintDestCountE, printDestChoices[def.dest].iniName);
s->model = (PrintModelE)prefsGetChoice(h, PRINT_SECTION, INI_MODEL, printModelChoices, PrintModelCountE, printModelChoices[def.model].iniName);
s->page.linesPerPage = prefsGetInt(h, PRINT_SECTION, INI_LINES, def.page.linesPerPage);
s->page.columns = prefsGetInt(h, PRINT_SECTION, INI_COLUMNS, def.page.columns);
s->page.marginTop = prefsGetInt(h, PRINT_SECTION, INI_MARGIN_TOP, def.page.marginTop);
s->page.marginBottom = prefsGetInt(h, PRINT_SECTION, INI_MARGIN_BOTTOM, def.page.marginBottom);
s->page.marginLeft = prefsGetInt(h, PRINT_SECTION, INI_MARGIN_LEFT, def.page.marginLeft);
s->page.marginRight = prefsGetInt(h, PRINT_SECTION, INI_MARGIN_RIGHT, def.page.marginRight);
s->page.tabWidth = prefsGetInt(h, PRINT_SECTION, INI_TAB, def.page.tabWidth);
s->page.wrap = prefsGetBool(h, PRINT_SECTION, INI_WRAP, def.page.wrap);
snprintf(s->filePath, sizeof(s->filePath), "%s", prefsGetString(h, PRINT_SECTION, INI_FILE, def.filePath));
platformPathNative(s->filePath);
snprintf(s->page.header, sizeof(s->page.header), "%s", prefsGetString(h, PRINT_SECTION, INI_HEADER, def.page.header));
snprintf(s->page.footer, sizeof(s->page.footer), "%s", prefsGetString(h, PRINT_SECTION, INI_FOOTER, def.page.footer));
if (printPageSetupProblem(&s->page)) {
s->page = def.page;
}
}
bool printSettingsSave(const PrintSettingsT *s) {
PrefsHandleT *h = prefsLoad(DVX_INI_PATH);
if (!h) {
return false;
}
printSettingsWrite(h, s);
bool ok = prefsSave(h);
prefsClose(h);
return ok;
}
void printSettingsWrite(PrefsHandleT *h, const PrintSettingsT *s) {
prefsSetString(h, PRINT_SECTION, INI_PORT, printDestChoices[s->dest].iniName);
prefsSetString(h, PRINT_SECTION, INI_FILE, s->filePath);
prefsSetString(h, PRINT_SECTION, INI_MODEL, printModelChoices[s->model].iniName);
prefsSetInt(h, PRINT_SECTION, INI_LINES, s->page.linesPerPage);
prefsSetInt(h, PRINT_SECTION, INI_COLUMNS, s->page.columns);
prefsSetInt(h, PRINT_SECTION, INI_MARGIN_TOP, s->page.marginTop);
prefsSetInt(h, PRINT_SECTION, INI_MARGIN_BOTTOM, s->page.marginBottom);
prefsSetInt(h, PRINT_SECTION, INI_MARGIN_LEFT, s->page.marginLeft);
prefsSetInt(h, PRINT_SECTION, INI_MARGIN_RIGHT, s->page.marginRight);
prefsSetInt(h, PRINT_SECTION, INI_TAB, s->page.tabWidth);
prefsSetBool(h, PRINT_SECTION, INI_WRAP, s->page.wrap);
prefsSetString(h, PRINT_SECTION, INI_HEADER, s->page.header);
prefsSetString(h, PRINT_SECTION, INI_FOOTER, s->page.footer);
}
// Paper first (it also clears "selected" on many printers), then off
// line, then a general error; a timeout only counts after a write.
bool printStatusProblem(uint8_t status, bool afterWrite, const char *where, char *out, int32_t outSize) {
const char *what = NULL;
if (status & PRINT_STATUS_PAPER_OUT) {
what = "is out of paper. Load paper, then choose Retry.";
} else if (!(status & PRINT_STATUS_SELECTED)) {
what = "is off line or switched off. Switch it on and put it on line, then choose Retry.";
} else if (status & PRINT_STATUS_IO_ERROR) {
what = "reports an error. Check that it is switched on, connected and has paper, then choose Retry.";
} else if (afterWrite && (status & PRINT_STATUS_TIMEOUT)) {
what = sNotResponding;
}
if (!what) {
return false;
}
snprintf(out, outSize, "The printer on %s %s", where, what);
return true;
}
// One byte to the port: wait (idle) while it is busy, ask on problems.
static bool sendByte(PrintJobT *job, uint8_t byte) {
PlatformTicksT busySince = platformClock();
char msg[PRINT_MSG_MAX];
while (true) {
uint8_t st = sOps->status(sOps->ctx, job->port);
if (printStatusProblem(st, false, job->where, msg, sizeof(msg))) {
if (!askRetry(job, msg)) {
return false;
}
busySince = platformClock();
continue;
}
if (!(st & PRINT_STATUS_NOT_BUSY)) {
if (platformClock() - busySince >= PLATFORM_MS_TO_TICKS(PRINT_BUSY_TIMEOUT_MS)) {
snprintf(msg, sizeof(msg), "The printer on %s %s", job->where, sNotResponding);
if (!askRetry(job, msg)) {
return false;
}
busySince = platformClock();
} else if (!idle(job)) {
return false;
}
continue;
}
st = sOps->write(sOps->ctx, job->port, byte);
if (printStatusProblem(st, true, job->where, msg, sizeof(msg))) {
if (!askRetry(job, msg)) {
return false;
}
busySince = platformClock();
continue;
}
job->bytes++;
return true;
}
}
// Write to the file; a short write (disk full) is a problem to retry.
static bool writeFile(PrintJobT *job, const uint8_t *data, int32_t len) {
int32_t done = 0;
char msg[PRINT_MSG_MAX];
while (done < len) {
int32_t n = (int32_t)fwrite(data + done, 1, (size_t)(len - done), job->fp);
done += n;
job->bytes += (uint32_t)n;
job->sinceIdle += n;
if (done < len) {
snprintf(msg, sizeof(msg), "Could not write to the file %s. The disk may be full. Make room, then choose Retry.", job->where);
if (!askRetry(job, msg)) {
return false;
}
}
}
if (job->sinceIdle >= job->idleEvery) {
return idle(job);
}
return true;
}