How to Build a Remote Barcode Scanner via MQTT

In scenarios like inventory management and checkout, we often need to get the barcodes into a PC device. We can use a handheld laser barcode scanner to scan the barcodes and send the results to a PC device via USB, Bluetooth or network. But as smartphones are ubiquitous now, we can also use them for such tasks.

In this article, we are going to create a solution that turns mobile phones into remote barcode scanners.

The solution contains a web barcode scanner and a Python desktop tool to receive the barcodes and trigger inputs.

MQTT is used as the communication protocol. It is an OASIS standard messaging protocol for the Internet of Things (IoT). It is designed as an extremely lightweight publish/subscribe messaging transport that is ideal for connecting remote devices with a small code footprint and minimal network bandwidth.1

Dynamsoft Camera Enhancer is used to capture camera frames and Dynamsoft Barcode Reader is used to read barcodes.

You can check out the video to see what it can do.

The scanner can insert the barcodes into an Excel file.

Build a Remote Barcode Scanner

Let’s do this in steps.

Create a Web Page which Communicates with a Python App Via MQTT

  1. Create a web page which connects to a public MQTT broker and publish a message to the barcode topic when connected using MQTT.js.

    <!DOCTYPE html>
    <html>
    <head>
      <title>Remote Barcode Scanner</title>
      <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
      <script src="https://cdn.jsdelivr.net/npm/mqtt/dist/mqtt.min.js"></script>
      <style>
      .app {
        position: fixed;
        transform: translateX(-50%);
        left: 50%;
        text-align: center;
      }
    
      .mqtt-settings {
        text-align: left;
      }
      </style>
    </head>
    <body>
    <div class="app">
      <h2>Remote Barcode Scanner</h2>
      <div class="mqtt-settings">
        <div>
          <label>Topic:</label>
          <input type="text" value="barcode" id="topic"/>
        </div>
        <div>
          <label>Status:</label>
          <span id="status"></span>
        </div>
      </div>
    </div>
    <script type="text/javascript">
    let client;
       
    init();
       
    function init(){
      connect();
    }
       
    function connect(){
      const clientId = 'mqttjs_' + Math.random().toString(16).substr(2, 8)
      let host;
      if (window.location.protocol === "https:") {
        host = 'wss://broker.emqx.io:8084/mqtt'
      }else{
        host = 'ws://broker.emqx.io:8083/mqtt'
      }
         
    
      const options = {
        keepalive: 60,
        clientId: clientId,
        protocolId: 'MQTT',
        protocolVersion: 4,
        clean: true,
        reconnectPeriod: 1000,
        connectTimeout: 30 * 1000,
        will: {
          topic: 'WillMsg',
          payload: 'Connection Closed abnormally..!',
          qos: 0,
          retain: false
        },
      }
    
      updateStatus('Connecting...');
      client = mqtt.connect(host, options)
    
      client.on('error', (err) => {
        console.log('Connection error: ', err)
        client.end()
        updateStatus(err);
      })
    
      client.on('reconnect', () => {
        updateStatus('Reconnecting...');
      })
    
      client.on('connect', () => {
        console.log('Client connected:' + clientId);
        const topic = document.getElementById("topic").value;
        client.publish(topic, "connected", { qos: 0, retain: false });
        updateStatus('Connected');
      })
    }
       
    function updateStatus(status){
      document.getElementById("status").innerText = status;
    }
    </script>
    </body>
    </html>
    
  2. Create a Python script which connects to the same public broker and subscribes to the barcode topic.

    import paho.mqtt.client as mqtt
    
    def on_connect(client, userdata, flags, rc):
        print(f"Connected with result code {rc}")
        client.subscribe("barcode")
    
    def on_message(client, userdata, msg):
        print(msg.topic+" "+str(msg.payload))
    
    client = mqtt.Client()
    client.on_connect = on_connect
    client.on_message = on_message
    client.connect("broker.emqx.io", 1883, 60)
    client.loop_forever()
    

Start the Python script first and then open the web page. Then, we can see the message is sent from the web page to the Python script.

Add Barcode Scanning Function to the Web Page

Next, make the web page a barcode scanner.

  1. Include the Dynamsoft Barcode Reader and Dynamsoft Camera Enhancer libraries.

    <script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.6.2/dist/dbr.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/dynamsoft-camera-enhancer@3.2.0/dist/dce.js"></script>
    
  2. Add a scanner element and a start scan button to start the scanner along with a continuous scan checkbox. A play button is used to control the decoding process and a close button is used to close the camera.

    <div class="app">
      <h2>Remote Barcode Scanner</h2>
      <div class="mqtt-settings">
        <div>
          <label>Topic:</label>
          <input type="text" value="barcode" id="topic"/>
        </div>
        <div>
          <label>Status:</label>
          <span id="status"></span>
        </div>
        <div>
          <label>Continuous scan:</label>
          <input type="checkbox" id="continuous"/>
        </div>
      </div>
      <button onclick="startScan();">Scan</button>
    </div>
    <div class="scanner" style="display:none;">
      <svg class="dce-bg-loading" viewBox="0 0 1792 1792"><path d="M1760 896q0 176-68.5 336t-184 275.5-275.5 184-336 68.5-336-68.5-275.5-184-184-275.5-68.5-336q0-213 97-398.5t265-305.5 374-151v228q-221 45-366.5 221t-145.5 406q0 130 51 248.5t136.5 204 204 136.5 248.5 51 248.5-51 204-136.5 136.5-204 51-248.5q0-230-145.5-406t-366.5-221v-228q206 31 374 151t265 305.5 97 398.5z"/></svg>
      <svg class="dce-bg-camera" viewBox="0 0 2048 1792"><path d="M1024 672q119 0 203.5 84.5t84.5 203.5-84.5 203.5-203.5 84.5-203.5-84.5-84.5-203.5 84.5-203.5 203.5-84.5zm704-416q106 0 181 75t75 181v896q0 106-75 181t-181 75h-1408q-106 0-181-75t-75-181v-896q0-106 75-181t181-75h224l51-136q19-49 69.5-84.5t103.5-35.5h512q53 0 103.5 35.5t69.5 84.5l51 136h224zm-704 1152q185 0 316.5-131.5t131.5-316.5-131.5-316.5-316.5-131.5-316.5 131.5-131.5 316.5 131.5 316.5 316.5 131.5z"/></svg>
      <div class="dce-video-container"></div>
      <div class="dce-scanarea">
      </div>
      <div class="sel-container">
        <select class="dce-sel-camera"></select>
        <select class="dce-sel-resolution"></select>
      </div>
      <button class="close-btn" onclick="stopScan();">Close</button>
      <div class="footer">
        <div class="play">
          <div class="play-button" onclick="toggleProcessing();"></div>
        </div>
      </div>
    </div>
    

    CSS:

    .footer {
      left: 0;
      bottom: 0;
      position: absolute;
      height: 6em;
      width: 100%;
      display: flex;
      align-items: center;
      flex-direction: row;
      justify-content: space-evenly;
    }
    
    .play {
      width: 4em;
      height: 4em;
      margin-top: calc(var(--shutter-size) / -2);
      margin-left: calc(var(--shutter-size) / -2);
      border-radius: 100%;
      background-color: rgb(198, 205, 216);
      padding: 12px;
      box-sizing: border-box;
      cursor: pointer;
    }
    
    .play-button {
      background-color: rgb(255, 255, 255);
      border-radius: 100%;
      width: 100%;
      height: 100%;
    }
    
    .play-button.running {
      background-color: rgb(255, 0,0);
      border-radius: 10%;
      width: 100%;
      height: 100%;
    }
    
    .play-button:active {
      background-color: rgb(220, 220, 220);
      border-radius: 100%;
      width: 100%;
      height: 100%;
    }
    
    .play-button.running:active {
      background-color: rgb(220, 220, 220);
      border-radius: 10%;
      width: 100%;
      height: 100%;
    }
          
    @keyframes dce-rotate{from{transform:rotate(0turn);}to{transform:rotate(1turn);}}
    @keyframes dce-scanlight{from{top:0;}to{top:97%;}}
    .scanner{width:100%;height:100%;min-width:100px;min-height:100px;background:#eee;position:absolute;left:0;top:0;}
    .dce-bg-loading{animation:1s linear infinite dce-rotate;width:40%;height:40%;position:absolute;margin:auto;left:0;top:0;right:0;bottom:0;fill:#aaa;}
    .dce-bg-camera{display:none;width:40%;height:40%;position:absolute;margin:auto;left:0;top:0;right:0;bottom:0;fill:#aaa;}
    .dce-video-container{position:absolute;left:0;top:0;width:100%;height:100%;}
    .dce-scanarea{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none;}
    .sel-container{position: absolute;left: 0;top: 0;}
    .sel-container .dce-sel-camera{display:block;}
    .sel-container .dce-sel-resolution{display:block;margin-top:5px;}
    .sel-container {display:block;margin-top:5px;}
    .close-btn{position: absolute;right: 0;top: 0;}
    
  3. Initialize Dynamsoft Camera Enhancer and bind its UI element to the scanner we created in the previous step.

    let camera;
    async function init() {
      updateStatus('Initializing...');
      camera = await Dynamsoft.DCE.CameraEnhancer.createInstance();
      await camera.setUIElement(document.getElementsByClassName("scanner")[0]);
      updateStatus('Initializd');
      connect();
    }
    
  4. Set a scan region for the Camera Enhancer so that it is easy for the user to aim to scan a barcode.

    camera.setScanRegion({
      regionLeft:0,
      regionTop:35,
      regionRight:100,
      regionBottom:65,
      regionMeasuredByPercentage: 1
    });
    
  5. Initialize Dynamsoft Barcode Reader with a license. You can apply for a license here.

    let reader;
    async function init() {
      Dynamsoft.DBR.BarcodeReader.license = 'DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=='; //one-day public trial
      reader = await Dynamsoft.DBR.BarcodeReader.createInstance();
    }
    
  6. Define functions to read barcodes from camera frames. It uses an interval to continuously scan barcodes.

    If a barcode is found, send the barcode text via MQTT. In addition, if the continuous scan is enabled, it will try to scan another barcode. Otherwise, it will stop decoding. Users have to use the play button to start decoding again.

    let interval;
    function toggleProcessing(){
      const playButton = document.getElementsByClassName("play-button")[0];
      if (interval) {
        playButton.classList.remove("running")
        console.log("stop");
        stopProcessingLoop();
      }else{
        console.log("start");
        playButton.classList.add("running");
        startProcessingLoop();
      }
    }
       
    function startProcessingLoop(){
      stopProcessingLoop();
      interval = setInterval(captureAndDecode,100); // read barcodes
    }
    
    function stopProcessingLoop(){
      if (interval) {
        clearInterval(interval);
        interval = undefined;
      }
      processing = false;
    }
       
    async function captureAndDecode() {
      if (!camera || !reader) {
        return
      }
      if (camera.isOpen() === false) {
        return;
      }
      if (processing == true) {
        return;
      }
      let frame = camera.getFrame();
      if (frame) {
        processing = true; // set processing to true so that the next frame will be skipped if the processing has not completed.
        let results = await reader.decode(frame);
        if (results.length > 0) {
          let barcode = results[0].barcodeText;
          Toastify({
            text: "Barcode found: "+barcode,
            duration: 2000,
            gravity: "top",
            position: "center"
          }).showToast(); //make a toast with https://github.com/apvarun/toastify-js
          publish(barcode);
          if (document.getElementById("continuous").checked) {
            const setProcessing = () => {
              processing = false;
            }
            setTimeout(setProcessing, 2000);
          }else{
            toggleProcessing();
          }
        }else{
          processing = false;
        }
      }
    };
    
  7. Define the startScan and closeScan functions.

    async function startScan(){
      const playButton = document.getElementsByClassName("play-button")[0];
      playButton.classList.add("running");
      await camera.open(true);
      startProcessingLoop();
    }
    
    function stopScan(){
      const playButton = document.getElementsByClassName("play-button")[0];
      playButton.classList.remove("running");
      stopProcessingLoop();
      camera.close(true);
    }
    

Trigger Barcode Input on the Desktop using the Python App

After receiving the barcode result, we can use PyAutoGUI and pyperclip to trigger input via clipboard and hotkeys.

We can set which keys to trigger and what text to add to the result in a config.py file first.

text_to_prepend = ""
text_to_append = ""
keys_to_prepend = []
keys_to_append = ["enter"]

Then, in the on_message function, copy the barcode result and trigger the paste hotkey for different platforms to make the input.

import pyautogui
import config
import pyperclip
import sys
import time
def on_message(client, userdata, msg):
    print(msg.topic+" "+str(msg.payload))
    barcode = msg.payload.decode()
    if config.text_to_prepend != "":
        barcode = config.text_to_prepend + barcode
    if config.text_to_append != "":
        barcode = barcode + config.text_to_append
    for key in config.keys_to_prepend:
        pyautogui.press(key)
    pyperclip.copy(barcode)
    time.sleep(0.5) // make sure the text is copied
    if sys.platform == 'darwin':
        pyautogui.hotkey('command', 'v')
    else:
        pyautogui.hotkey('ctrl', 'v')
    for key in config.keys_to_append:
        pyautogui.press(key)

All right, we’ve now finished the remote barcode scanner solution.

Source Code

Get the source code of the demo to have a try:

https://github.com/tony-xlh/remote-barcode-scanner-mqtt

References