// 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 #include #include 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; }