DEV Community

mprattinger
mprattinger

Posted on

Modernizing my All-in-One printer

I have an 10-year-old all-in-on printer/scanner in my home office. I was thinking of replacing it with a new shiny network enabled one. As my wife and i working at home with our company laptops, we have to change the cable permanently from one device to the other. Printing and scanning on both devices via the network would be a nice thing.
We still have some ink laying around and therefore we will use this one until everything is used.

But i still wanted the network stuff. So, i tried it with an raspberry pi.

Printing was easy, activating cups and setup the printer and i was good to go and it worked as i expected.

Scanning was the more interesting part. I installed sane and all the utilities. Now scanning on the pi works but will i get to documents or how will i start the scan process?
GPIO and a button to the rescue. I decided to write a little node program, that will scan a document and store it on a shared drive. So, i will be able to access the documents.

The flow of the program is:

  1. Initializing the hardware (Button and a status led (red))
  2. Preparing services to control the scanning cli and the conversion to pdf
  3. Watching the button. When the button is pressed start the scan process and switch on the status led.

I also added a mail service to email the scanned documents to me and a second led to tell when the device is ready (green).

scantool_Steckplatine

Initializing the hardware

I will show the hardware class from the button as an example. Via onoff a watch event is used to watch the button on GPIO18. When the button is pressed, an event is fired. The Led and the services are listening to this event.

import { Gpio } from "onoff";
import { EventEmitter } from "events";
import { loggers, Logger } from "winston";

import { EventNames } from "../Helper/Helper";

export class ScanButton{
    eventEmitter: EventEmitter
    logger: Logger;

    scanButton: Gpio;
    scanning: boolean = false;

    constructor(eventEmitter: EventEmitter) {
        this.eventEmitter = eventEmitter;

        this.logger = loggers.get("scantoollogger");

        this.scanButton = new Gpio(18, "in", "rising", { debounceTimeout: 10})
        this.scanButton.watch(this.buttonClicked.bind(this))   

        this.eventEmitter.on(EventNames.ScanBegin, () => { 
            this.scanning = true; 
        });
        this.eventEmitter.on(EventNames.ScanFinished, () => this.scanning = false);

        this.eventEmitter.on(EventNames.Cleanup, () => this.scanButton.unexport());
    }   

    buttonClicked() {
        if(this.scanning) {
            this.logger.info("Scan already in progress!")
            return;
        }
        this.eventEmitter.emit(EventNames.ScanButtonPressed);
    }
}
Enter fullscreen mode Exit fullscreen mode

When a scan is started, i block the possibillity to start another scan until the previous is finished.

Scan a document

The scan service is called from the eventlistener in the main file (index.ts) and starts to scan.

import EventEmitter from "events";
import { stat, mkdir, rm } from "fs";
import { loggers, Logger } from "winston";
import { EventNames } from "../Helper/Helper";
import { exec } from "child_process";
import dateformat from "dateformat";

export class ScanService {
    readonly tempDir = "./tmp";
    readonly scanDir = "/scans";

    eventEmitter: EventEmitter;
    logger: Logger;

    constructor(ev: EventEmitter) {
        this.eventEmitter = ev;
        this.logger = loggers.get("scantoollogger");
    }

    public scanDocument(): Promise<string> {
        return new Promise(async (res, rej) => {
            try {
                this.logger.info("Sending begin...");
                this.eventEmitter.emit(EventNames.ScanBegin);

                this.logger.info("Checking if tmp dir exists...");
                await this.checkScanDir();
                this.logger.info("Done!");

                this.logger.info("Scanning document to temp folder...");
                await this.scanit();
                this.logger.info("Done!");

                this.logger.info("Converting scan to pdf...");
                let scannedDocument = await this.convertToPDF();
                this.logger.info(`Done! Document was ${scannedDocument}`);

                this.logger.info("Cleaning up temp folder...");
                await this.cleanup();
                this.logger.info("Done!");

                res(scannedDocument);
            } catch (ex) {
                rej(ex);
            }
        });
    }

    private async checkScanDir(): Promise<boolean> {
        return new Promise((res, rej) => {
            stat(this.tempDir, (err) => {
                if (!err) {
                    this.logger.info("Dir exists");
                    res(true);
                }
                else if (err.code === "ENOENT") {
                    mkdir(this.tempDir, err => {
                        if (err) rej(err);
                        else res(true);
                    });
                }
            });
        });
    }

    private async scanit(): Promise<boolean> {
        return new Promise((res, rej) => {
            let command = `cd ${this.tempDir} && scanimage -b --batch-count 1 --format png -d 'pixma:04A91736_31909F' --resolution 150`;

            exec(command, (err, stdout, stderr) => {
                if(err) {
                    rej(`Error calling command ${command} (${err}). ${stderr}`);
                } else {
                    this.logger.info(`Scan command (${command}) called. Output was ${stdout}`);
                    res(true);
                }
            });
        });
    }

    private async convertToPDF(): Promise<string> {
        return new Promise((res, rej) => {
            let fname = dateformat(new Date(), "yyyy-mm-dd_HH-MM-ss");
            let docPath = `${this.scanDir}/${fname}.pdf`
            let command = `cd ${this.tempDir} && convert *.png ${docPath}`;

            exec(command, (err, stdout, stderr) => {
                if(err) {
                    rej(`Error calling command ${command} (${err}). ${stderr}`);
                } else {
                    this.logger.info(`Convert command (${command}) called. Output was ${stdout}`);
                    res(docPath);
                }
            });
        });
    }

    private async cleanup(): Promise<boolean> {
        return new Promise((res, rej) => {
            try {
                rm(this.tempDir, { recursive: true, maxRetries: 5}, (err) => {
                    if(err) throw err;
                    else res(true);
                });
            } catch (ex) {
                rej(`Error cleaning up the output folder: ${ex}`);
            }
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

First, we check if the scanto directory exists (temp). Then the scan command is called via exec. After the scan is completed the scanned png is converted to a pdf and copied to the scan folder, which is a shared folder on the pi. After all this is done, the temp folder is cleaned.

Sending the pdf via email

During the development of the program i decided to also send the pdf via email to my mailbox. There is an easy package called nodemailer where i use the smtp settings of my outlook.com mailbox to send the pdf.

import nodemailer from "nodemailer";
import EventEmitter from "events";
import { loggers, Logger } from "winston";
import * as dotenv from "dotenv";
import Mail from "nodemailer/lib/mailer";

export class Mailer {

    eventEmitter: EventEmitter;
    logger: Logger;
    transport: Mail;

    constructor(ev: EventEmitter) {
        this.eventEmitter = ev;
        this.logger = loggers.get("scantoollogger");

        let envPath = __dirname+'/../../.env';
        this.logger.info(`EnvPath ist ${envPath}`)
        dotenv.config({ path: envPath});

        this.logger.info(`Creating transport for mailing...`)
        try {
            this.transport = nodemailer.createTransport({
                host: process.env.MAILSMTP,
                port: Number(process.env.MAILSMTPPORT),
                auth: {
                    user: process.env.MAILUSERNAME,
                    pass: process.env.MAILPASSWORD
                }
            });
        } catch(ex) {
            this.logger.error(`Error creating mail transport: ${ex}`);
            throw ex;
        }
        this.logger.info(`Transport created!`)
    }

    public sendMail(document: string): Promise<boolean> {
        return new Promise(async (res, rej) => {
            try {
                var docItems = document.split("/");
                var doc = docItems[docItems.length - 1];

                this.logger.info(`Sending email...`);
                await this.transport.sendMail({
                    from: "mprattinger@outlook.com",
                    to: "mprattinger@outlook.com",
                    subject: `Ihr scan: ${doc}`,
                    html: '<h1>Anbei Ihr gescanntes Dokument</h1><p>Vielen Dank das sie ein Dokument gescannt haben!</p>',
                    attachments: [
                        {
                            filename: doc,
                            path: document
                        }
                    ]
                });
                this.logger.info(`Mail sent!`);

                res(true);
            } catch(ex) {
                rej(ex);
            }
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

After all these steps are done, we fire the finished event. This will enable the scan again and also turn of the progress led.

And thats it. One step is missing, i want to draw and 3d print a case with the button and the led, so it looks nice next to my printer.

The code is availiable at github: https://github.com/mprattinger/ScanTool/blob/1d27c40f7d/src/Services/Mailer.ts

Top comments (1)

Collapse
 
brandonwallace profile image
brandon_wallace

It is nice that you were able to have an old printer from the garbage dump. I bet you had fun working on this project.