DEV Community

Imam Ali Mustofa for Street Community Programmer

Posted on • Updated on

Street Programmer: Print Receipt From Online POS Web App to Local Thermal Printer

Before I tell you more about how I created a Websocket server with PHP to provide communication access between online web apps and local thermal printers, I would like to express my gratitude to donors who have provided support for what I'm doing here.

Donors:

GitHub Sponsors:

Previous story...


Hello punk! I am Freestyler, many question a had on internet about how to print from online web app to local printer "thermal"?. Yep! Thermal Printer that usually used in POS Application, sound like whaaaaat.... (just kidding, nothing!)

How To Do That

Sorry... am narcissistic (in my head) person but yeah (am health BTW). So, what is my receipt?

The Receipt (Requirements)

In a long time ago am PHP Developer and then I have backstreet relationship (try to make it word like Hacker) with JavaScript (and falling in love with it) and now I am Playgrammer.

The Framework

  • React JS My young fiance
  • Workerman My old lady that can create WebSocket Server with easy!

Both of them it's just like Documentation.

When it's good, it's very good. When it's bad, it's better than nothing. - Dick Brandon

The Dependencies

  • React useWebSocket Hook WebSocket Client to talk with his mama
  • ESCPOS-PHP blank paper that client can wrote their homework and share with his mama
  • Dexie.js IndexedDB client to store printer setting (optional)

The Devices

Additional Requirement

  • Coffee - Required!
  • Ciggarettes - Required!
  • Music that you hate so much! - Required!

Open The Door

Tok tok tok... open the door please. Open your unusual Text Editor and open terminal window then create your brand new react app! (FYI: I will not tell you how to create react brand new react app, just read their documentation!)

After create brand new react app, then install react-use-websocket using composer (it's wrong way dude!).

This is just (F) example that I used in my project so don't ask in different way. Just wrote in your way of imagination.

Create Printer Setting Storage

// Filename: src/db/index.js

import Dexie from 'dexie';

export const db = new Dexie('YOUR-STORAGE-NAME');

db.version(1).stores({
    printer_settings: '++id, app_name, printer_name, interface, font_type, ch_line_1, ch_line_2, ch_line_3, cf_line_1, cf_line_2, cf_line_3, cl_operator, cl_time, cl_trx_number, cl_costumer_name, cl_discount, cl_total_discount, cl_total, cl_tax, cl_member, cl_paid, cl_return, cl_loan, cl_saving, cl_tempo, line_feed_each_in_items, column_width, more_new_line, pull_cash_drawer, template'
});

Enter fullscreen mode Exit fullscreen mode

Create Printer Setting Page

Create printer setting page to allow user setting their own language and how receipt will look like.

The printer setting page contains field like in Dexie Storage definition.

// Filename: src/constants/index.js
// NOTE:
// - cl = Custom Language
// - ch = Custom Header
// - cf = Custom Footer

export const DEFAULT_PRINTER_SETTING = {
    app_name: 'Your Application Name',
    printer_name: '/dev/usb/lp0',
    interface: 'linux-usb',
    font_type: 'C',
    ch_line_1: 'Your Header Line 1',
    ch_line_2: 'Your Header Line 2',
    ch_line_3: 'Your Header Line 3',
    cf_line_1: 'Your Footer Line 1',
    cf_line_2: 'Your Footer Line 2',
    cf_line_3: 'Your Footer Line 3',
    cl_operator: 'Operator Name',
    cl_time: 'Time',
    cl_trx_number: 'Transaction Number',
    cl_customer_name: 'Customer Name',
    cl_discount: 'Discount',
    cl_total_discount: 'Total Discount',
    cl_total: 'Total',
    cl_tax: 'Tax',
    cl_member: 'Member',
    cl_paid: 'Paid',
    cl_return: 'Return',
    cl_debt: 'Debt',
    cl_saving: 'Saving',
    cl_due_date: 'Due date',
    line_feed_each_in_items: 15,
    column_width: 40,
    more_new_line: 1,
    pull_cash_drawer: 'no',
    template: 'epson'
}

const interfaceOptions = [
    { value: 'cpus', key: 'CPUS' },
    { value: 'ethernet', key: 'Ethernet' },
    { value: 'linux-usb', key: 'Linux USB' },
    { value: 'smb', key: 'SMB' },
    { value: 'windows-usb', key: 'Windows USB' },
    { value: 'windows-lpt', key: 'Windows LPT' },
]

const fontTypeOptions = [
    { value: 'A', key: 'Font Type A' },
    { value: 'B', key: 'Font Type B' },
    { value: 'C', key: 'Font Type C' },
]
Enter fullscreen mode Exit fullscreen mode

Create form that provide user to input settings. You can create with Formik or something that you know to create form in react.

In my (F) case, I use Formik and make DEFAULT_PRINTER_SETTING constant as form initialValues and use interfaceOptions and fontTypeOptions to create dropdown options in form.

Create Form Submit Handler

As you can see in above, form need to submit to store setting in Dexie Storage.

// Filename: src/pages/PrinterSetting.js
import { db } from '../db'
import { DEFAULT_PRINTER_SETTING } from '../constants'

const formOnSubmit = async (values) => {
    try {
        db.printer_settings.count(count => {
            if (count > 0) {
                db.printer_settings.update(1, values).then(updated => {
                    if (updated)
                        console.log("Printer setting was updated!");
                    else
                        console.warn("Nothing was updated - there were no update changed");
                });
            } else {
                db.printer_settings.add(DEFAULT_PRINTER_SETTING).then(() => {
                    console.log("New setting just created using default setting");
                }).catch(error => {
                    console.error(error)
                })
            }
        })
    } catch (error) {
        console.error(error)
    }
}
Enter fullscreen mode Exit fullscreen mode

After creating printer setting page, don't forget to drink your coffee and fire your ciggarettes, play next song that you hate so much in loud volume!

Create POS Context

To make life easy... create POS Context and Provider in react. Place it and wrap you app on it.

...For What?

We need to make WebSocket Client that share it in whole app:

  • Share WebSocket Connection Status
  • Create Indicator
  • Send Message to WebSocket Server
// Filename: src/contexts/POSContext.js
import useWebSocket from 'react-use-websocket'
import { db } from '../db'
import { DEFAULT_PRINTER_SETTING } from '../constants'

const POSContext = createContext()

const SOCKET_URL = 'ws://<your-websocket-address:port>'

const generateAsyncUrlGetter = (url, timeout = 2000) => () => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(url);
        }, timeout);
    });
};

const POSProvider = ({ children }) => {
    const [currentSocketUrl, setCurrentSocketUrl] = useState(SOCKET_URL)
    const [printerSettings, setprinterSettings] = useState()

    const { sendMessage, sendJsonMessage, readyState } = useWebSocket(
        currentSocketUrl,
        {
            share: true,
            shouldReconnect: () => true,
            reconnectAttempts: 3,
            reconnectInterval: 3000,
            onError: (event) => {
                alert.show('WebSocket trying to connect to sever but failed!', {
                    type: 'error',
                    position: 'bottom right',
                    timeout: 3000
                })
            },
            onOpen: (event) => {
                alert.show('WebSocket connection establised!', {
                    type: 'success',
                    position: 'bottom right',
                    timeout: 3000
                })
            },
            onClose: (event) => {
                alert.show('WebSocket connection is closed!', {
                    type: 'warning',
                    position: 'bottom right',
                    timeout: 3000
                })
            },
            onReconnectStop: () => 6
        }
    )

    const readyStateString = {
        0: 'CONNECTING',
        1: 'OPEN',
        2: 'CLOSING',
        3: 'CLOSED',
    }[readyState]

    const reconnectWebSocket = useCallback(() => {
        if (readyState === 3) {
            setCurrentSocketUrl(generateAsyncUrlGetter(SOCKET_URL))
        } else {
            setCurrentSocketUrl(null)
        }
    }, [setCurrentSocketUrl, readyState])

    const initData = useCallback(async () => {
        if (readyState === 1) {
            db.printer_settings.count(count => {
                if (count > 0) {
                    setprinterSettings(first(settings))
                    console.log('Setting is exists')
                } else {
                    setprinterSettings(DEFAULT_PRINTER_SETTING)
                    console.log('Setting is not exists!')
                }
            })
        } else {
            console.log('Setting is not exists!')
            setprinterSettings(DEFAULT_PRINTER_SETTING)
        }
    }, [readyState])

    useEffect(() => {
        if (readyState === 1) initData()
    }, [readyState, initData])

    return (
        <KasirContext.Provider value={{
            sendMessage,
            sendJsonMessage,
            reconnectWebSocket,
            readyStateString,
            printerSettings
        }}>
            {children}
        </KasirContext.Provider>
    )
}

export { KasirContext, KasirProvider }
Enter fullscreen mode Exit fullscreen mode

Drink coffee and smoke! More loud volume!

Dance More

Create Indicator Button

This button showing WebSocket connection between Client and Server. This button also can trigger to reconnect to server when connection is closed.

// Filename: src/components/IndicatorButton.js

import React, { useContext } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { POSContext } from '../contexts/POSContext';

function IndicatorButton() {
    const posCtx = useContext(POSContext)

    return (
        <button
            className='btn btn-indicator'
            onClick={() => posCtx.reconnectWebSocket()}
        >
            <FontAwesomeIcon icon="fa-solid fa-satellite-dish" />{' '}
            Printer Server is:{' '}
            <span
                className={`${posCtx.readyStateString === 'CONNECTING' ? 'text-info' : (posCtx.readyStateString === 'OPEN') ? 'text-success' : (posCtx.readyStateString === 'CLOSED') ? 'text-secondary' : 'text-danger'}`}
            >
                {posCtx.readyStateString}
            </span>
        </button>
    )
}

export default IndicatorButton
Enter fullscreen mode Exit fullscreen mode

You can place it that component somewhere in you code.

Now! The WebSocket Client is running but connection has been close for 3 times attempt reconnecting to WebSocket Server.

So what next?!

Create WebSocket Server

Creating websocket server using workerman is like magic.

  1. Create new PHP index.php file inside printer-server directory
  2. Visit Workerman Repository
  3. Install workerman and escpos-php package inside printer-server directory using npm (that's dumb!)
  4. copy and paste A simple websocket example
  5. go to your bus terminal and run php index.php start

That's it, nothing else! and you can't do any printing call with that code. It's easy right?

WTF! Are you kidding me?!
WTF! Are you kidding me?! (That's sound great BTW. Thank you)

Don't mad me! Just Kidding Please!

Now update websocket server script with this code! I promise you... (My head sound: $^U@%G@YT@#.....)

But before you touch websocket server script you need to know what data that printer server needed?

Am Indonesian so don't mad me if my source code contains any Indonesian Language that mixed with English Language. You can translate Indonesia > English using Translator btw or after that you can learn more Indonesian Language (Hahaha just kidding! Next!)

Payload That WebSocket Server (Printer Server Needed)

{
    "trx": {
        "nama_kasir": "Umam",
        "no_transaksi": "nomor-transaksi",
        "trx": "only-number-trx",
        "nama_pelanggan": "nama-pelanggan",
        "tgl_transaksi": "tanggal-transaksi-format-yyyy-mm-dd",
        "diskon": "jumlah-diskon",
        "total_transaksi": "total-transaksi",
        "uang_diterima": "uang-diterima",
        "uang_kembali": "uang-kembali",
        "jenis_pembayaran": "jenis-pembayaran",
        "jatuh_tempo": "tanggal-jatuh-tempo",
        "uang_titip": "jumlah-uang-titip",
        "ekspedisi": null,
        "ongkir": null,
        "pajak": "0",
        "potongan_member": "0"
    },
    "text_kembalian": 0,
    "belanja": [
        {
            "barang_nama": "MIE SEDAP SOTO",
            "harga": "0",
            "jumlah": "50 Dus",
            "sub_total": "5125000",
            "initial_code": "BRG-605A5B07C3EE5",
            "no_transaksi": "nomor-transaksi"
        },
        {
            "barang_nama": "MIE SEDAP GORENG",
            "harga": "107000",
            "jumlah": "38 Dus",
            "sub_total": "4066000",
            "initial_code": "BRG-605A5B01530C4",
            "no_transaksi": "nomor-transaksi"
        },
        {
            "barang_nama": "PATI SG",
            "harga": "0",
            "jumlah": "100 Sak",
            "sub_total": "25450000",
            "initial_code": "BRG-625678D10EC80",
            "no_transaksi": "nomor-transaksi"
        }
    ],
    "alamat_gudang": "Jl. AHMAD YANI - DESA SAMBIROTO - TAYU - PATI <br>\r\nDepan Masjid Baitussalam <br>\r\n0853 - 2622 - 5333 / 0856 - 4165 - 5202",
    "app_name": "CV. Kalimasodo Agriculture",
    "nama_lengkap": "Umam",
    "from": "posclient",
    "command": "posclient",
    "printer_name": "EPSON TM-U220 Receipt",
    "printer_settings": {
        "printer_name": "/dev/usb/lp0",
        "interface": "linux-usb",
        "font_type": "A",
        "custom_print_header": [
            "Your Company Name",
            "Your Subheading for Receipt",
            "Your Third text on header"
        ],
        "custom_print_footer": [
            "Your Company Name",
            "Your Subheading for Receipt",
            "Your Third text on footer"
        ],
        "custom_language": {
            "operator": "Operator",
            "time": "Time",
            "trx_number": "TRX Number",
            "costumer_name": "Costumer",
            "discount": "Discount",
            "total_discount": "Total Disc" ,
            "total": "Total",
            "tax": "Tax",
            "member": "Member",
            "paid": "Paid",
            "return": "Return",
            "loan": "Loan",
            "saving": "Saving",
            "tempo": "Tempo"
        },
        "line_feed_each_in_items": 1,
        "column_width": 48,
        "more_new_line": 1,
        "pull_cash_drawer": false
    }
}
Enter fullscreen mode Exit fullscreen mode

You can format that request payload as you want with that schema.

Create WebSocket Printer Server

Following is all the code in index.php

<?php

use Workerman\Worker;
use Mike42\Escpos\Printer;
use Mike42\Escpos\PrintConnectors\DummyPrintConnector;
use Mike42\Escpos\PrintConnectors\CupsPrintConnector;
use Mike42\Escpos\PrintConnectors\NetworkPrintConnector;
use Mike42\Escpos\PrintConnectors\FilePrintConnector;
use Mike42\Escpos\PrintConnectors\WindowsPrintConnector;
use Mike42\Escpos\CapabilityProfile;

require_once __DIR__ . '/vendor/autoload.php';

// Create a Websocket server
$ws_worker = new Worker('websocket://0.0.0.0:2112');

// Emitted when new connection come
$ws_worker->onConnect = function ($connection) {
    echo "New connection\n";
};

// Emitted when data received
$ws_worker->onMessage = function ($connection, $payload) {
    // Send hello $data
    $data = json_decode($payload, TRUE);
    $options = $data['printer_settings'];

    echo '> metadata: ' . $data['from'] . PHP_EOL;
    sendingMessage($connection, createMessage('info', 'metadata: ' . $data['from']));
    echo '> activer_printer: ' . $data['printer_name'] . PHP_EOL;
    sendingMessage($connection, createMessage('info', 'active_printer: ' . $data['printer_name']));
    echo '< echo', "\n";
    print_r($options);

    if ($data['from'] === 'posclient') {
        print_reciept($data, $options);
    } else if ($data['from'] === 'testprinter') {
    }
};

// Emitted when connection closed
$ws_worker->onClose = function ($connection) {
    echo "Connection closed\n";
};

// Run worker
Worker::runAll();

function print_reciept($arrData, $options = null)
{
    // Printer Connector
    $allowed_print_interfaces = ['cpus', 'ethernet', 'linux-usb', 'smb', 'windows-usb', 'windows-lpt'];
    if (in_array($options['interface'], $allowed_print_interfaces)) {
        switch ($options['interface']) {
            case 'cpus':
                $connector = new CupsPrintConnector($options['printer_name']);
                break;
            case 'ethernet':
                $connector = new NetworkPrintConnector($options['printer_name'], 9100);
                break;
            case 'linux-usb':
                $connector = new FilePrintConnector($options['printer_name']);
                break;
            case 'smb':
            case 'windows-usb':
            case 'windows-lpt':
            default:
                $connector = new WindowsPrintConnector($options['printer_name']);
                break;
        }
    } else {
        // Make sure you load a Star print connector or you may get gibberish.
        $connector = new DummyPrintConnector();
        $profile = CapabilityProfile::load("TSP600");
    }

    // Font Type
    $selectPrinterFont = [
        'A' => Printer::FONT_A,
        'B' => Printer::FONT_B,
        'C' => Printer::FONT_C
    ];

    if ($options['template'] === 'epson') {
        $fontSettings = [
            'header_nota' => $selectPrinterFont['B'],
            'sub_header_nota' => $selectPrinterFont['B'],
            'detail_operator' => $selectPrinterFont['C'],
            'detail_balanja' => $selectPrinterFont['C'],
            'sub_promo' => $selectPrinterFont['B'],
            'footer' => $selectPrinterFont['C'],
            'sub_footer' => $selectPrinterFont['B']
        ];
    } else {
        $fontSettings = [
            'header_nota' => $selectPrinterFont['C'],
            'sub_header_nota' => $selectPrinterFont['C'],
            'detail_operator' => $selectPrinterFont['C'],
            'detail_balanja' => $selectPrinterFont['C'],
            'sub_promo' => $selectPrinterFont['C'],
            'footer' => $selectPrinterFont['C'],
            'sub_footer' => $selectPrinterFont['C']
        ];
    }

    echo "Line feed each item: " . $options['line_feed_each_in_items'];

    $printer = new Printer($connector);
    $column_width = ($options['template'] === 'epson') ? 40 : 48;
    $divider    = str_repeat("-", $column_width) . "\n";
    $optnl = ($options['template'] === 'epson') ? 0 : $options['more_new_line'];
    $more_new_line = str_repeat("\n", $optnl);

    // Membuat header nota
    $printer->initialize();
    $printer->selectPrintMode(Printer::MODE_DOUBLE_HEIGHT); // Setting teks menjadi lebih besar
    $printer->setJustification(Printer::JUSTIFY_CENTER);    // Setting teks menjadi rata tengah
    $printer->setFont($fontSettings['header_nota']);
    $printer->setEmphasis(true);
    $printer->text($arrData['app_name'] . "\n");
    $printer->setEmphasis(false);
    $printer->feed(1);

    // Membuat sub header nota
    $printer->initialize();
    $printer->setJustification(Printer::JUSTIFY_CENTER); // Setting teks menjadi rata tengah
    $printer->setFont($fontSettings['sub_header_nota']);
    $printer->setEmphasis(true);
    if (count($options['custom_print_header']) > 0) {
        for ($i = 0; $i < count($options['custom_print_header']); $i++) {
            $header_text = str_replace('\r\n', "", $options['custom_print_header'][$i]);
            $header_text = str_replace('<br>', PHP_EOL, $header_text);
            $printer->text(strtoupper($header_text) . "\n");
        }
    } else {
        $printer->text("MELAYANI SEPENUH HATI\n");
        $printer->text(strtoupper(str_replace("<br>", "", $arrData['alamat_gudang'])) . "\n");
        $printer->text("Buka Jam 07:30 - 16:30\n");
    }
    $printer->setEmphasis(false);
    $printer->feed(1);

    // Membuat detail operator
    $printer->initialize();
    $printer->setLineSpacing(20);
    $printer->setFont($fontSettings['detail_operator']);
    $printer->setEmphasis(true);
    $printer->text($divider);
    $printer->text(buatBarisSejajar($options['custom_language']['operator'] . " : ", $arrData['nama_lengkap'] . $more_new_line, $column_width));
    $printer->text(buatBarisSejajar($options['custom_language']['time'] . " : ", $arrData['trx']['tgl_transaksi'] . ' ' . date('H:i:s')  . $more_new_line, $column_width));
    $printer->text(buatBarisSejajar($options['custom_language']['trx_number'] . " : ", $arrData['trx']['trx'] . $more_new_line, $column_width));
    $printer->text(buatBarisSejajar($options['custom_language']['costumer_name'] . " : ", $arrData['trx']['nama_pelanggan'] . $more_new_line, $column_width));
    $printer->text($divider);
    $printer->setEmphasis(false);
    $printer->feed(1);

    // Print Detail Belanja
    $printer->initialize();
    $printer->setLineSpacing(20);
    $printer->setFont($fontSettings['detail_balanja']);
    $printer->setEmphasis(true);
    $no    = 1;
    foreach ($arrData['belanja'] as $b) {
        $printer->text(buatBarisContent($b['barang_nama'], idr_format($b['sub_total'], false), $options['template']));
        $printer->text('@ ' . idr_format($b['harga'], true) . " x " . $b['jumlah'] . "\n");
        $printer->feed($options['line_feed_each_in_items']);
        $no++;
    }
    $printer->setEmphasis(false);
    $printer->feed(1);

    if (count($arrData['promo']) > 0) {
        // Membuat sub promo
        $printer->initialize();
        $printer->setLineSpacing(20);
        $printer->feed(1);
        $printer->setJustification(Printer::JUSTIFY_CENTER); // Setting teks menjadi rata tengah
        $printer->setFont($fontSettings['sub_promo']);
        $printer->setEmphasis(true);
        $printer->text($divider);
        for ($i = 0; $i < count($arrData['promo']); $i++) {
            $printer->text(wordwrap($arrData['promo'][$i], 35, "\n") . "\n\n");
        }
        $printer->setEmphasis(false);
        $printer->feed(3);
    }

    // Print Footer
    $printer->initialize();
    $printer->setLineSpacing(20);
    $printer->setFont($fontSettings['footer']);
    $printer->setEmphasis(true);
    $printer->text($divider);

    // Total Belanja
    if ($arrData['trx']['sebelum_diskon'] != $arrData['trx']['total_transaksi']) {
        $printer->text(buatBarisSejajar('Total Belanja', idr_format($arrData['trx']['sebelum_diskon']), $column_width) . $more_new_line);
    }

    // Total Pajak
    if ($arrData['trx']['pajak'] != 0) {
        $printer->text(buatBarisSejajar($options['custom_language']['tax'], idr_format($arrData['trx']['pajak']), $column_width) . $more_new_line);
    }
    // Total Diskon
    if ($arrData['trx']['diskon'] != 0) {
        $printer->text(buatBarisSejajar($options['custom_language']['discount'], idr_format($arrData['trx']['diskon']), $column_width) . $more_new_line);
    }
    // Total Potongan Member
    if ($arrData['trx']['potongan_member'] != 0) {
        $printer->text(buatBarisSejajar($options['custom_language']['member'], $arrData['trx']['potongan_member'], $column_width) . $more_new_line);
    }

    $printer->text(buatBarisSejajar($options['custom_language']['total'], idr_format($arrData['trx']['total_transaksi']), $column_width) . $more_new_line);

    // Total Bayar
    $bayar = ($arrData['trx']['uang_diterima'] != 0) ? number_format($arrData['trx']['uang_diterima']) : '-';
    $printer->text(buatBarisSejajar($options['custom_language']['paid'], $bayar, $column_width) . $more_new_line);

    $text_kembalian = ($arrData['trx']['jenis_pembayaran'] != 'Non Tunai') ? number_format($arrData['trx']['uang_kembali']) : '-';

    $printer->text(buatBarisSejajar($options['custom_language']['return'], $text_kembalian, $column_width) . $more_new_line);
    // End Total Kembalian
    if ($arrData['trx']['jenis_pembayaran'] == 'Non Tunai') {
        $printer->text(buatBarisSejajar($options['custom_language']['tempo'], date('d/m/Y', strtotime($arrData['trx']['jatuh_tempo'])), $column_width) . $more_new_line);
        $printer->text(buatBarisSejajar($options['custom_language']['saving'], number_format($arrData['trx']['uang_titip']), $column_width) . $more_new_line);
        $printer->text(buatBarisSejajar($options['custom_language']['loan'], number_format($arrData['trx']['total_transaksi'] - $arrData['trx']['uang_titip']), $column_width) . $more_new_line);
    }
    $printer->setEmphasis(false);
    $printer->feed(1);

    // Membuat sub footer nota
    $printer->initialize();
    // $printer->setLineSpacing(20);
    $printer->feed(1);
    $printer->setJustification(Printer::JUSTIFY_CENTER); // Setting teks menjadi rata tengah
    $printer->setFont($fontSettings['sub_footer']);
    $printer->setEmphasis(true);
    if (count($options['custom_print_footer']) > 0) {
        $printer->text("Terimakasih sudah berbelanja di\n");
        for ($i = 0; $i < count($options['custom_print_footer']); $i++) {
            $printer->text($options['custom_print_footer'][$i] . "\n");
        }
    } else {
        $printer->text("Terimakasih sudah berbelanja di\n");
        $printer->text("CV. Kalimosodo Angriculture\n");
        $printer->text("Barang yang sudah dibeli tidak bisa\n");
        $printer->text("dikembalikan.\n");
    }
    $printer->setEmphasis(false);
    $printer->feed(3);

    $printer->cut();

    // Get the data out as a string
    // $data = $connector -> getData();
    // echo $data . PHP_EOL;

    /* Pulse */
    if ($options['pull_cash_drawer']) {
        $printer->pulse();
    }

    // Close the printer when done.
    $printer->close();
}

function sendingMessage($connection, $message)
{
    $connection->send($message);
}

function createMessage($type = 'info', $message)
{
    $timestamps = date('Y/m/d H:i:s');
    $log = strtoupper($type);
    return "[$timestamps][$log] $message";
}

function buatBarisSejajar($kolom1, $kolom2, $x)
{
    $divider    = str_repeat("-", $x);
    $full_width = strlen($divider);
    $half_width = $full_width / 2;

    $rawText = str_pad($kolom1, $half_width);
    $rawText .= str_pad($kolom2, $half_width, " ", STR_PAD_LEFT);
    return $rawText . "\n";
}

function buatBarisContent($kolom1, $kolom2, $template)
{
    $paperLength   = ($template === 'epson') ? 40 : 48;
    $divider       = str_repeat("-", $paperLength);
    $full_width    = strlen($divider);
    $half_width    = $full_width / 2;
    $total_x1 = ($template === 'epson') ? 18 : 22;
    $total_x2 = ($template === 'epson') ? 21 : 25;
    $lebar_kolom_1 = $total_x1; // asale 18
    $lebar_kolom_2 = $total_x2; // asale 21

    // Melakukan wordwrap(), jadi jika karakter teks melebihi lebar kolom, ditambahkan \n
    $kolom1 = wordwrap($kolom1, $lebar_kolom_1, "\n", true);
    $kolom2 = wordwrap($kolom2, $lebar_kolom_2, "\n", true);

    // Merubah hasil wordwrap menjadi array, kolom yang memiliki 2 index array berarti memiliki 2 baris (kena wordwrap)
    $kolom1Array = explode("\n", $kolom1);
    $kolom2Array = explode("\n", $kolom2);

    // Mengambil jumlah baris terbanyak dari kolom-kolom untuk dijadikan titik akhir perulangan
    $jmlBarisTerbanyak = max(count($kolom1Array), count($kolom2Array));

    // Mendeklarasikan variabel untuk menampung kolom yang sudah di edit
    $hasilBaris = array();

    // Melakukan perulangan setiap baris (yang dibentuk wordwrap), untuk menggabungkan setiap kolom menjadi 1 baris
    for ($i = 0; $i < $jmlBarisTerbanyak; $i++) {

        // memberikan spasi di setiap cell berdasarkan lebar kolom yang ditentukan,
        $hasilKolom1 = str_pad((isset($kolom1Array[$i]) ? $kolom1Array[$i] : ""), $lebar_kolom_1);
        $hasilKolom2 = str_pad((isset($kolom2Array[$i]) ? $kolom2Array[$i] : ""), $lebar_kolom_2, " ", STR_PAD_LEFT);

        // Menggabungkan kolom tersebut menjadi 1 baris dan ditampung ke variabel hasil (ada 1 spasi disetiap kolom)
        $hasilBaris[] = $hasilKolom1 . " " . $hasilKolom2;
    }

    // Hasil yang berupa array, disatukan kembali menjadi string dan tambahkan \n disetiap barisnya.
    return implode("\n", $hasilBaris) . "\n";
}

function idr_format($number, $prefix = false, $decimal_number = 0, $decimal_point = ',', $thousand_point = '.')
{
    return ($prefix == false) ? number_format($number, $decimal_number, $decimal_point, $thousand_point) : 'Rp. ' . number_format($number, $decimal_number, $decimal_point, $thousand_point);
}
Enter fullscreen mode Exit fullscreen mode

To run the above code we need to use the following command in the command line, with this command:

➜ php index.php start
Workerman[index.php] start in DEBUG mode
------------------------------------------- WORKERMAN --------------------------------------------
Workerman version:4.1.4          PHP version:7.4.33           Event-Loop:\Workerman\Events\Select
-------------------------------------------- WORKERS ---------------------------------------------
proto   user            worker          listen                      processes    status           
tcp     darkterminal    none            websocket://0.0.0.0:2112    1             [OK]            
--------------------------------------------------------------------------------------------------
Press Ctrl+C to stop. Start success.
Enter fullscreen mode Exit fullscreen mode

If you want something more efficient and don't repeat the command above, please use supervisord, systemd, or nssm (NSSM - the Non-Sucking Service Manager for Windows users)


If you find this blog is useful and open your mind to another side of the world of IT and want to give me a cup of coffee or become a sponsor on my GitHub account:

Buy Me ☕ Become Sponsor ❤

For sponsors/donors, your name will be included in the upcoming article, so please make sure to provide your username from your DEV.to, Twitter, Github, or any other social media accounts you have.

Ask Me Anything 💬

Top comments (0)