Smart Reader over MQTT

MQTT (or MQTT/TLS) is the preferred communication channel for Smart Reader operation.

Two MQTT topics are involved:

  • springcard/springcore/{$id}/rdr/evt for reader to host communication (events)
  • springcard/springcore/{$id}/rdr/cmd for host to reader communication (commands)

The messages preferably use the JSON format. Text messages using the $SCRDR format are supported as well but may be less efficient in term of host-side implementation.

Configuration checklist

  • Define the IPv4 configuration of the device in register 0280. Leave this register empty to get the configuration from DHCP.
  • Set register 02C0 to 64 (MQTT Client, Smart Reader operation)
  • Set register 02A0 to 00xxxx02xx (JSON format over the Network link)
  • Insert the name or the address of the MQTT broker (server) into register 0286
  • Enable TLS in register 0288 or leave this register empty for plain mode
  • (Optionaly) Confirm the server port in register 0285. If this register is empty, default port 1883 is used for plain mode or default port 8883 for TLS mode
  • (Optionaly) Precize the client login register 028B and password register 028C. Leave empty if the broker allows anonymous access or relies on the TLS client certificate.
  • (Optionaly) Precize the client options in register 0289. Default values shall be suitable for most MQTT brokers

Sample host software

Use the following Python script as a reference for using the Smart Reader with configuration JSON over MQTT.

Pre-requisites

  • Set-up an MQTT broker and allow anonymous access or adapt the script accordingly
  • Install Paho MQTT library (command: python -m pip install paho-mqtt)

Using the script

  • Specify the address or the hostname of the MQTT broker using the --addr= parameter,
  • Provide the --wink, --accept or --deny parameters if you want the script to send a command to the reader in response to every event.

Source code

# libraries import
from threading import Event
import paho.mqtt.client as mqtt
import logging
import getopt
import tempfile
import json
import time
import ssl
import sys
import os
import re

# helpers import
from helpers.mqtt_events import mqtt_events
from helpers.ctrlccatcher import CtrlCCatcher


# Incoming message callback
def incoming_message( client, mqtt_context, msg ):

    # sanity check
    if ( client == None ) or ( msg == None ):
        return

    logging.info( "Incoming message on topic " + msg.topic )
    logging.info( str(msg.payload) )

    # try load this json content
    try:
        json_content = json.loads( msg.payload )
    except json.JSONDecodeError:
        logging.warning( "This is not a json message!" )
        return
    
    # json message is correct, check if we can find the TagId field
    if mqtt_context[ "reply" ] :
        logging.info( "Sending reply: " + str(mqtt_context[ "reply" ]) )

        # change for the command topic
        reply_topic = re.sub( "/evt$", "/cmd", msg.topic )
        logging.info( "Reply topic is " + reply_topic )
        client.publish( topic=reply_topic, payload=mqtt_context[ "reply" ] )


# display_help function
def display_help( script_name ):

    print( script_name )
    print( "-s, --addr=<server>             MQTT server (default is localhost)" )
    print( "-p, --port=<port>               MQTT port (default is 1883 for plain, 8883 for TLS)" )
    print( "[--tls]                         Use secure communication (implicit if CA, certificate or key are set)" )
    print( "[--ca=<file>]                   CA certificate file" )
    print( "[--cert=<file>]                 Client's certificate file" )
    print( "[--key=<file>]                  Client's private key file" )
    print( "[--clientid]                    MQTT Client ID" )
    sys.exit()


# main function  
def main( script_name, argv ):

    # custom settings for AC demo
    current_path = os.path.dirname( os.path.realpath( __file__ ) )
    addr = "localhost"
    port = 0
    use_tls = False    
    keepalive = 60
    mqtt_context = {}
    client_id = "scrdr_" + next(tempfile._get_candidate_names())

    mqtt_context[ "current_path" ] = current_path
    mqtt_context[ "reply" ] = False
    

    # options parsing
    try:
        opts, _ = getopt.getopt(argv,"h",[ "help", "addr=","port=","ca=","cert=","key=","tls","clientid=","wink","accept", "deny" ])
    except getopt.GetoptError as e:
        print(e)
        display_help( script_name )

    for opt, arg in opts:
        if opt in ( "-h", "--help"):
            display_help( script_name )
        elif opt == "--addr":
            addr = arg
        elif opt == "--ca":
            use_tls = True
            ca = arg
        elif opt == "--cert":
            use_tls = True
            cert = arg
        elif opt == "--key":
            use_tls = True
            key = arg
        elif opt == "--tls":
            use_tls = True
        elif opt == "--clientid":
            client_id = arg            
        elif opt == "--wink":
            mqtt_context[ "reply" ] = b"{ \"Sequence\": \"0E\" }"                
        elif opt == "--accept":
            mqtt_context[ "reply" ] = b"{ \"Sequence\": \"60\" }"
        elif opt == "--deny":
            mqtt_context[ "reply" ] = b"{ \"Sequence\": \"61\" }"
        elif opt == "--port":
            try:
                port = int(arg)
            except ValueError:
                print( "Port must be an integer number!" )
                sys.exit()
                
    # port?
    if use_tls and port <= 0:
        port = 8883
    else:
        port = 1883

    # set logging options
    logging.basicConfig(level=logging.DEBUG)

    # SSL options
    if use_tls:
        try:
            #debug print opnessl version
            logging.info( f"using {ssl.OPENSSL_VERSION}" )
            ssl_context = ssl.create_default_context()
            ssl_context.load_verify_locations( cafile=ca )
            ssl_context.load_cert_chain( certfile=cert, keyfile=key )
        except Exception:
            print( "Could not create TLS context!" )
            sys.exit()


    # CTRL+C catcher thread (this could be useful on some Windows platforms)
    ctrlccatch = CtrlCCatcher()
    ctrlccatch.start()

    # process exit flag
    mqtt_context[ "exit_event" ] = Event()
    mqtt_context[ "exit_event" ].clear()    

    # callback declaration
    mqtt_context[ "callback" ] = incoming_message

    # select on which topics to subscribe (topic, qos)
    mqtt_context[ "subscriptions" ] = [
        ( "springcard/springcore/+/rdr/evt", 0 ),
    ]
    
    # start our MQTT client process
    client = mqtt.Client( client_id=client_id )
    if use_tls:
        client.tls_set_context( context=ssl_context )
    client.user_data_set( mqtt_context )
    client.on_connect = mqtt_events.on_connect
    client.on_disconnect = mqtt_events.on_disconnect
    client.on_message = mqtt_events.on_message

    logging.info( f"Trying to connect to {addr} on port {port}" )
    logging.info( f"Client ID is {client_id}" )        
    try:
        client.connect( host=addr, port=port, keepalive=keepalive )
    except ( TimeoutError, ConnectionRefusedError ):
        logging.error( "Unable to contact this server!" )
        sys.exit()

    # start mqtt processing thread
    client.loop_start()

    # this is the main loop, it does nothing but wait for a quit indication
    while ctrlccatch.wait() and not mqtt_context[ "exit_event" ].is_set():
        # no need to eat all cpus
        time.sleep(0.5)

    # stop the client
    client.loop_stop()

    # stop all process and exit
    logging.info( "Cleaning..." )
    ctrlccatch.terminate()
    ctrlccatch.join()
    logging.info( "Done." )

  
# entry point
if __name__ == "__main__":
    # add "this script" folder to the library search path
    # could be useful on Windows platform
    sys.path.append( os.path.dirname( os.path.realpath( __file__ ) ) )
  
    # start our application
    main( os.path.basename( __file__ ), sys.argv[1:] )
    
# EOF