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

ProspexAI
7 min readMay 20, 2024

In This post, we’ll walk through the process of connecting a thermal printer or any external devices like barcode reader, passport scanner to a Windows OS using Django. i am using 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 https://github.com/tarek421995/POS-Printer

One more thing before starting you should install the drivers that comes with this printer. other models does not require any driver to build a program to use them by using pyusb library, more expensive devices has product_id and vendor_id.

At the end i will brief detail about using 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

Step 2: Installing Required Packages

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

pip install django pywin32

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

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',
]

## replace the template with this or directly add the template folder in DIRS
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',
],
},
},
]

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}"

Run the migrations to create these models in the database:

python manage.py makemigrations pos
python manage.py migrate

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)

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})

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>

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>

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'),
]

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')),
]

Step 8: Running the Project

Start the Django development server:

python manage.py runserver

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 requier 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()

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

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

Bouns Section:

i have create docker composing and dockerfile for building 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"]

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

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.

I will answer any questions in this regards.

Happy printing and device integration!!

Do not forgot to support me with nice comment and following.

--

--

ProspexAI

Our mission is to educate and inspire individuals, from tech enthusiasts to professionals, about the potential and applications of cutting-edge technologies