DEV Community

ProspexAI
ProspexAI

Posted on

Connecting a POS Printer to Windows OS Using Django: A Comprehensive Guide

In this post, we’ll walk through the process of connecting a thermal printer or any external devices like a barcode reader, passport scanner to a Windows OS using Django. I am using the Oscar POS purchased from Amazon. We’ll cover setting up a virtual environment, creating a Django project, and building an interface for dynamic printing using win32print.

The project code can be found here: GitHub Repository

Before starting, you should install the drivers that come with this printer. Other models do not require any driver to build a program to use them by using the pyusb library. More expensive devices have product_id and vendor_id.

At the end, I will provide a brief detail about using the pyusb library and connecting devices to it.

Prerequisites

Before we begin, ensure you have the following:

  • Python installed on your system
  • Django installed in your Python environment
  • A POS thermal printer for Invoices and Queue Tickets with the necessary drivers installed on your Windows OS

Step 1: Setting Up the Virtual Environment

First, we’ll create a virtual environment for our project. Open your command prompt and run the following commands:

# Install virtualenv if you haven't already
pip install virtualenv

# Create a virtual environment named 'printer_env'
virtualenv printer_env

# Activate the virtual environment
# On Windows
printer_env\Scripts\activate

# On macOS/Linux
source printer_env/bin/activate
Enter fullscreen mode Exit fullscreen mode

Step 2: Installing Required Packages

Within the virtual environment, install Django and other required packages:

pip install django pywin32
Enter fullscreen mode Exit fullscreen mode

Step 3: Setting Up the Django Project

Create a new Django project and app:

# Create a Django project named 'printer_project'
django-admin startproject printer_project

# Navigate to the project directory
cd printer_project

# Create a Django app named 'pos'
python manage.py startapp pos
Enter fullscreen mode Exit fullscreen mode

Add the new app to your INSTALLED_APPS in printer_project/settings.py:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'pos',
]

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / "templates"],  # Add this line
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
Enter fullscreen mode Exit fullscreen mode

Step 4: Setting Up Models

Define the models for saving sequence numbers and printer preferences. In pos/models.py, add:

from django.db import models

class Sequence(models.Model):
    number = models.IntegerField(default=0)

class PrinterPreference(models.Model):
    device_name = models.CharField(max_length=255)
    device_id = models.CharField(max_length=255, unique=True)
    is_default = models.BooleanField(default=False)

    def __str__(self):
        return f"{self.device_name} - Default: {self.is_default}"
Enter fullscreen mode Exit fullscreen mode

Run the migrations to create these models in the database:

python manage.py makemigrations pos
python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

Step 5: Setting Up the Admin Interface

Register your models in pos/admin.py:

from django.contrib import admin
from .models import PrinterPreference, Sequence

admin.site.register(PrinterPreference)
admin.site.register(Sequence)
Enter fullscreen mode Exit fullscreen mode

Step 6: Creating Views and Templates

Create views for handling sequence numbers and listing USB devices in pos/views.py:

from django.http import JsonResponse, HttpResponse
from django.shortcuts import redirect, render
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import csrf_exempt
from .models import Sequence, PrinterPreference
import win32print
import win32com.client
import pythoncom
import logging

def home(request):
    return render(request, "sequence.html")

@csrf_exempt  # For demonstration purposes only, consider CSRF in production
@require_http_methods(["POST"])
def sequence_view(request):
    number_to_print = request.POST.get('number_to_print')
    if 'reset' in request.POST:
        Sequence.objects.update(number=0)
        return JsonResponse({'number': 0, 'status': 'Sequence reset'})

    sequence = Sequence.objects.get(id=1)
    if number_to_print:
        try:
            number_to_print = int(number_to_print)
            sequence.number = number_to_print
            sequence.save()
            response = print_sequence(sequence.number)
            return JsonResponse({'number': number_to_print, 'status': 'Sequence Setting Success', **response})
        except ValueError:
            return JsonResponse({'status': 'Invalid number'}, status=400)
    else:
        sequence.number += 1
        sequence.save()
        response = print_sequence(sequence.number)
    return JsonResponse({'number': sequence.number, **response})

def print_sequence(number):
    data = b'\x1b\x40'
    data += b'\x1b\x37\x06\x15\x10'
    data += b'\x1b\x21\x30'
    data += 'Your Company Name\n'.encode('utf-8')
    data += b'\x1b\x21\x00'
    data += 'Registration\n'.encode('utf-8')
    data += b'\x1b\x21\x10'
    data += 'INFO : \n'.encode('utf-8')
    data += b'--------------------------------\n'
    data += b'\x1b\x21\x30'
    data += 'TOKEN NO:{}\n'.format(number).encode('utf-8')
    data += b'\x1d\x56\x42\x00'
    data += b'\x1B\x42\x00\x00'
    try:
        default_printer = PrinterPreference.objects.filter(is_default=True).first()
        if not default_printer:
            return {"status": "error", "message": "No default printer selected."}

        printer_name = default_printer.device_name
        printer_handle = win32print.OpenPrinter(printer_name)
        try:
            job_info = ("Token Print", None, "RAW")
            job_id = win32print.StartDocPrinter(printer_handle, 1, job_info)
            win32print.StartPagePrinter(printer_handle)
            win32print.WritePrinter(printer_handle, data)
            win32print.EndPagePrinter(printer_handle)
            win32print.EndDocPrinter(printer_handle)
        finally:
            win32print.ClosePrinter(printer_handle)
        return {"status": "success", "message": f"Printed sequence number {number}"}
    except Exception as e:
        return {"status": "error", "message": f"An error occurred: {e}"}

logging.basicConfig(level=logging.DEBUG)

def list_devices():
    try:
        pythoncom.CoInitialize()
        wmi = win32com.client.Dispatch("WbemScripting.SWbemLocator")
        wmi_service = wmi.ConnectServer(".", "root\cimv2")
        devices = wmi_service.ExecQuery("SELECT * FROM Win32_PnPEntity")
        device_list = [{'device_name': device.Name} for device in devices]
        return device_list
    except pythoncom.com_error as e:
        return [{'error': 'Failed to list devices due to a WMI error.'}]
    finally:
        pythoncom.CoUninitialize()

@csrf_exempt  # For demonstration purposes only, consider CSRF in production
@require_http_methods(["GET", "POST"])
def list_usb_devices(request):
    if request.method == "POST":
        default_printer_name = request.POST.get('default_printer')
        if default_printer_name:
            PrinterPreference.objects.all().update(is_default=False)
            our_printer, created = PrinterPreference.objects.get_or_create(device_name=default_printer_name)
            if our_printer:
                our_printer.is_default = True
                our_printer.save()
            return redirect('pos:home')
    device_list = list_devices()
    if not device_list:
        device_list = [{'error': "No USB devices found."}]
    return render(request, 'list_usb_devices.html', {'devices': device_list})
Enter fullscreen mode Exit fullscreen mode

Create the templates sequence.html and list_usb_devices.html in pos/templates/:

sequence.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Sequence Number</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.bundle.min.js"></script>
    <style>
        .btn-pos { background-color: orange; color: white; padding: 20px 40px; font-size: 1.5rem; margin: 10px 0; }
        .btn-pos:hover { background-color: darkorange; }
        .input-pos { font-size: 1.5rem; padding: 10px

; }
        .logo-corner { position: absolute; top: 10px; left: 10px; }
        .container { max-width: 800px; }
    </style>
    <script>
        $(document).ready(function() {
            $('form').on('submit', function(e) {
                e.preventDefault();
                var numberToPrint = $('#number_to_print').val();
                var reset = $('button[name="reset"]').is(':focus');
                var postData = {'csrfmiddlewaretoken': '{{ csrf_token }}'};
                if (reset) {
                    postData['reset'] = true;
                } else {
                    postData['number_to_print'] = numberToPrint;
                }
                $.ajax({
                    type: 'POST',
                    url: "{% url 'pos:sequence_view' %}",
                    data: postData,
                    success: function(response) {
                        if (response.status === "success" || response.status === "Sequence reset") {
                            $('#number').text(response.number);
                            if (reset) {
                                $('form')[0].reset();
                            }
                        } else if (response.status === "error") {
                            alert("Error: " + response.message);
                        }
                    },
                    error: function(error) {
                        console.log(error);
                        alert('An error occurred.');
                    }
                });
            });
        });
    </script>
</head>
<body>
    <div class="logo-corner">
        <img src="https://eu-central-1.linodeobjects.com/roadslink/images/file.png" alt="Logo" height="60">
    </div>
    <div class="container mt-5">
        <h1 class="mb-3">Sequence Number: <span id="number">{{ number }}</span></h1>
        <form method="post" class="text-center">
            {% csrf_token %}
            <div class="form-group">
                <input type="text" class="form-control input-pos" id="number_to_print" name="number_to_print" placeholder="Enter/Set a number to print">
            </div>
            <button type="submit" name="print_next" class="btn btn-pos btn-block">Print Next Number</button>
            <button type="submit" name="reset" class="btn btn-pos btn-block">Reset</button>
        </form>
        <a href="{% url 'pos:list_usb_devices' %}">Select your device if not running</a>
    </div>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

list_usb_devices.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>List USB Devices</title>
</head>
<body>
    <h1>Select Default Printer</h1>
    {% if error %}
        <p style="color: red;">Error: {{ error }}</p>
    {% endif %}
    <form method="post">
        {% csrf_token %}
        <ul>
            {% for device in devices %}
                <li>
                    <label>
                        <input type="radio" name="default_printer" value="{{ device.device_name }}">
                        {{ device.device_name }}
                    </label>
                </li>
            {% endfor %}
        </ul>
        <button type="submit">Set as Default Printer</button>
    </form>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Step 7: Setting Up URL Routes

Define URL routes for your views in pos/urls.py:

from django.urls import path
from .views import home, list_usb_devices, sequence_view

app_name = 'pos'
urlpatterns = [
    path('', home, name='home'),
    path('sequence_view', sequence_view, name='sequence_view'),
    path('list_usb_devices/', list_usb_devices, name='list_usb_devices'),
]
Enter fullscreen mode Exit fullscreen mode

Include the pos app URLs in the main project URL configuration in printer_project/urls.py:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('pos.urls')),
]
Enter fullscreen mode Exit fullscreen mode

Step 8: Running the Project

Start the Django development server:

python manage.py runserver
Enter fullscreen mode Exit fullscreen mode

Navigate to http://127.0.0.1:8000/ in your web browser. You should see the sequence number interface where you can print the next number or reset the sequence. Use the link http://127.0.0.1:8000/list_usb_devices to select and set your default printer.

Testing with pyusb

Using this method is more product support but requires verified product factory who register his devices in all operating systems like Windows or Linux.

pip install pyusb
import usb.core
import usb.util

def list_usb_devices():
    # Find all connected USB devices
    devices = usb.core.find(find_all=True)

    # Iterate over each device and print information
    for device in devices:
        print(f"Device: {device}")
        print(f"  - idVendor: {hex(device.idVendor)}")
        print(f"  - idProduct: {hex(device.idProduct)}")
        print(f"  - Manufacturer: {usb.util.get_string(device, device.iManufacturer)}")
        print(f"  - Product: {usb.util.get_string(device, device.iProduct)}")
        print(f"  - Serial Number: {usb.util.get_string(device, device.iSerialNumber)}")
        print()

if __name__ == "__main__":
    list_usb_devices()
Enter fullscreen mode Exit fullscreen mode

Result:

Device: DEVICE ID 1d6b:0003 on Bus 004 Address 001
 =================
 bLength                :   0x12 (18 bytes)
 bDescriptorType        :    0x1 Device
 bcdUSB                 :  0x310 USB 3.1
 bDeviceClass           :    0x9 Hub
 bDeviceSubClass        :    0x0
 bDeviceProtocol        :    0x3
 bMaxPacketSize0        :    0x9 (9 bytes)
 idVendor               : 0x1d6b
 idProduct              : 0x0003
 bcdDevice              :  0x602 Device 6.02
 iManufacturer          :    0x3 Error Accessing String
 iProduct               :    0x2 Error Accessing String
 iSerialNumber          :    0x1 Error Accessing String
 bNumConfigurations     :    0x1
  CONFIGURATION 1: 0 mA
  ====================================
   bLength              :    0x9 (9 bytes)
   bDescriptorType      :    0x2 Configuration
   wTotalLength         :   0x1f (31 bytes)
   bNumInterfaces       :    0x1
   bConfigurationValue  :    0x1
   iConfiguration       :    0x0 
   bmAttributes         :   0xe0 Self Powered, Remote Wakeup
   bMaxPower            :    0x0 (0 mA)
    INTERFACE 0: Hub
    =======================================
     bLength            :    0x9 (9 bytes)
     bDescriptorType    :    0x4 Interface
     bInterfaceNumber   :    0x0
     bAlternateSetting  :    0x0
     bNumEndpoints      :    0x1
     bInterfaceClass    :    0x9 Hub
     bInterfaceSubClass :    0x0
     bInterfaceProtocol :    0x0
     iInterface         :    0x0 
      ENDPOINT 0x81: Interrupt IN
      ==========================
       bLength          :    0x7 (7 bytes)
       bDescriptorType  :    0x5 Endpoint
       bEndpointAddress :   0x81 IN
       bmAttributes     :    0x3 Interrupt
       wMaxPacketSize   :    0x4 (4 bytes)
       bInterval        :    0xc

- idVendor: 0x1d6b
- idProduct: 0x3
Enter fullscreen mode Exit fullscreen mode

Part 2: I will make another article about pyusb with a passport scanner.

Bonus Section

I have created Docker composing and a Dockerfile for building an image that will work in any Operating Systems.

Docker Installation

To run this project in a Docker container, use the following Dockerfile and docker-compose.yml.

Dockerfile

FROM python:3.11

# Install dependencies
RUN apt-get update && apt-get install -y \
    usbutils \
    libusb-1.0-0-dev \
    && rm -rf /var/lib/apt/lists/*

# Set work directory
WORKDIR /app

# Install Python dependencies
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt

# Copy project files
COPY . .

# Set the environment variable for Django settings
ENV DJANGO_SETTINGS_MODULE=printer.settings

# Expose port 8000
EXPOSE 8000

# Run the Django server
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
Enter fullscreen mode Exit fullscreen mode

docker-compose.yml

version: '3.10'

services:
  web:
    build: .
    command: sh -c "python manage.py runserver 0.0.0.0:8000"
    volumes:
      - .:/app
    ports:
      - "8000:8000"
    environment:


 - DJANGO_SETTINGS_MODULE=printer.settings
    devices:
      - "/dev/bus/usb:/dev/bus/usb"
    privileged: true
Enter fullscreen mode Exit fullscreen mode

Conclusion

In this post, we covered setting up a virtual environment, creating a Django project, and building an interface for dynamic printing using win32print. By following these steps, you can connect and use a thermal printer with a Django web application on Windows OS. This setup allows for flexible and dynamic printing solutions tailored to your specific needs.

Feel free to customize the code and templates to suit your project requirements. If you have any questions or run into issues, leave a comment below, and I’ll be happy to help.

Happy printing and device integration!!

Top comments (0)