Using CloudLocate with M8/M9 GNSS

Introduction 

The purpose of this guide is to learn how to use CloudLocate in Device to Service mode using real-time GNSS raw measurements  from u-blox GNSS M8/M9

Prerequisites

Before reading this guide:

Note: if you are not familiar with GNSS configuration, you can initially collect the raw measurement using u-center (See this guide)  and then come back to this guide 

Set-up the connection with GNSS

You can use any program for serial communication with the GNSS EVK. In the example below we used Realterm on Windows 10.

Step 1 : connect the GNSS evaluation kit to your computer using USB. Once you’ve connected, windows will automatically recognize the device and will install necessary drivers. 

Step 2: open Realterm software, and by default the “Display” tab will be selected. Even without any configuration you will see  some data flowing. Change the display to “Hex[space]”. 

Next, on the “Port” tab (next to Display), set the following: 

Once you’ve configured Port settings (as above), click “Open” button, and it will open a channel with GNSS module. 

Note: depending on the EVK that you are using, the baud rate can be different. In order to find which is your EVK's baud rate, see the corresponding  products user guide. 

Step 3: now that the communication with the EVK has been successfully established, you can configure the GNSS to generate the UBX-RXM-MEASX message and send it out to the configured port .  

0xB5 0x62 0x06 0x01 0x03 0x00 0x02 0x14 0x01 0x21 0x6E 

To send this command, select “Send” tab, and paste the above hex in the text box, and then click “Send Numbers”. This will enable MEASX message on GNSS receiver. 

Note: in the case that after enabling the message, you do not see any data being sent from the module, you may have to enable periodic messages. To do that, send UBX-NAV-STATUS message with following parameters (in hex) 

0xB5 0x62 0x01 0x03 0x00 0x00 0x04 0x0D  

Note: to disable the UBX-RXM-MEASX message send the following command: 

0xB5 0x62 0x06 0x01 0x03 0x00 0x02 0x14 0x00 0x20 0x6D 

Note: The configuration is not persistent. In order to make it persistent, please see Receiver configuration Section 3 on Page 9 of Protocol specification document .

Note: In order to get an understanding of how the commands are structured, and created, and what are the commands available, please use the above mentioned protocol specification document and look for the 3 message format used for the configuration

Sample code

The above section allows you to  understanding on how to configure the GNSS module and get the raw measurement (UBX-RXM-MEASX) using GNSS receiver. However,  in a real use case, you need to filter the real-time messages coming from the GNSS to validate the quality of the satellite signals and avoid no-fix condition, and also to apply a fallback strategy if the quality of the signal is not good enough. The main goal is balance the power consumption versus the location accuracy and to avoid no-fix situations in the cloud

u-blox provides a sample code for host processor that allows you modify few parameters to achieve the best result according to your strategy. 

The script:

The sample code for M8 (and M9) is available in the  Download area of the Thingstream portal in the IoT Location-as-a- service > CloudLocate section. 

The next sections provides more details about the code, the settings and the fallback options

Note: the code is provided just as reference and you are allowed to modify it accordingly to your needs

General settings

In this section you can find the explanation of the general settings to connect to GNSS and the custom parameters to optimize the validate the quality the satellite information contained in the UBX-RXM-MEASX message so that you can filter out low quality signals.

COMPORT = "COM4"

BAUD_RATE = 9600

GNSS_TYPE = "GPS"

TIMEOUT = 10

CNO_THRESHOLD = 25

MIN_NO_OF_SATELLITES = 5

MULTIPATH_INDEX = 2

PSEUDO_RANGE_ERR = 39

EPOCHS = 1

SEND_BINARY = False


FALLBACK_METHODOLOGY = FallbackConfig.FALLBACK_DO_NOT_SEND

where:

It's worth remembering that a MEASX message comprises 44 bytes for the header plus 24 bytes for each satellite.

An acceptable index to start testing can be 39.

Capturing raw measurements

Once you have adjusted the parameters, you can capture real-time UBX-RXM-MEASX measurements using the following code snippet:

with serial.Serial(COMPORT, BAUD_RATE, timeout= TIMEOUT) as ser:

    #enable MEASX message on the receiver

    ser.write(MEASX_ENABLE_MESSAGE)

    # even though it is possible to get first message as a valid MEASX message

    # but we are not sure, and cannot rely on the first message to be so

    # so, we always ignore first message

    rawMessage = ser.read_until(MEASX_HEADER)    

    startTime = time.time()

    # continue reading MEASX messages until:

    # [a] timeout happens

    # [b] you get number of messages as per configuration

    # [c] save this measx message, along with satellite info so it can be used for fallback logic

    while (time.time()-startTime) <= TIMEOUT and validMessageCounter < EPOCHS :

        # read message from receiver

        rawMessage = ser.read_until(MEASX_HEADER)

        # calculate size of payload contained inside read MEASX message

        size = (rawMessage[1]<<8)|rawMessage[0]

        #checking size for a valid measx message

        if(size <= 33) :

            print(f"Message skipped: {rawMessage.hex()}")

            #skipping this message

            continue

        # extract actual MEASX payload (without header, and checksum)

        measxMessagePayload = rawMessage[2:size+2]

        # get the number of satellites contained in this message

        numSv = measxMessagePayload[34]

        print("Number of satellites: ", numSv)

        # need to save this message for fallback logic

        processedMeasxMessage = {

            'measxMessage': rawMessage[0:size+4],

            'maxCNO': 0,

            'satellitesInfo': { }

        }

        satelliteCount = 0

        # for the number of satellites contained in the message

        # we need to see if every satellite's data falls

      # as per our configuration

        # because a single MEASX message can contain 

        # more than one satellite's information

        for i in range(0, numSv):

            # only accept the message if it fulfills our criteria, 

            # based on configuration parameters above

            gnss = measxMessagePayload[44+24*i]

            if gnss == CONSTELLATION_TYPES[GNSS_TYPE]: 

                # psuedoRangeRMS error index

                psuedoRange = measxMessagePayload[65 + 24 * i]

                # carrier-to-noise ratio

                cNO = measxMessagePayload[46+24*i]

                if cNO > processedMeasxMessage['maxCNO']:

                    processedMeasxMessage['maxCNO'] = cNO

                multipathIndex = measxMessagePayload[47+24*i]

                svID = measxMessagePayload[45+24*i]

                processedMeasxMessage['satellitesInfo'][svID] = {'cno': cNO, 'mpi': multipathIndex }

                if cNO >= CNO_THRESHOLD and multipathIndex <= MULTIPATH_INDEX and psuedoRange <= PSEUDO_RANGE_ERR:    

                    satelliteCount = satelliteCount + 1

                    print(f"gnss: {gnss} ... svID:{svID} ... cNO: {cNO} ... multipathIndex: {multipathIndex} ... psuedoRange:{psuedoRange}")

        # saving processed message for fallback logic            

        READ_RAW_MEASX_MESSAGES.append(processedMeasxMessage)


        if satelliteCount >= MIN_NO_OF_SATELLITES:

            MEASX_MESSAGE.extend(MEASX_HEADER)

            MEASX_MESSAGE.extend(bytearray(rawMessage[0:size+4]))

            validMessageCounter = validMessageCounter + 1

The above code snippet does the following:

Send the raw measurement to the service

Now that you are capturing  a real-time UBX-RXM-MEASX message, you need to send it to CloudLocate endpoint to get the position. For this, as mentioned in the CloudLocate getting started guide, you should already have a CloudLocate Thing.  The following code snippet converts MEASX message into a base64 encoded string, establishes an MQTT connection with the server, sends the data, and gets the position back on the device. Optionally, as described in the Getting Started guide, you can get the position from your application platform.

# we need to publish our base64 encoded message on this topic

    MQTT_PUB_TOPIC = "CloudLocate/GNSS/request"

    # we will get a position back on this topic, so we need to subscribe to it

    MQTT_SUB_TOPIC = f"CloudLocate/{DeviceID}/GNSS/response"


    # create JSON payload, which will be sent to CloudLocate, exit if it exceeds 8KB

    if SEND_BINARY:

        # Sending RAW binary message

        MQTT_MSG = MEASX_MESSAGE

    else:

        # convert our MEASX byte-array into base64 encoded string

        BASE64_ENC_PAYLOAD = base64.b64encode(MEASX_MESSAGE).decode()

        MQTT_MSG = f"{{\"body\": \"{BASE64_ENC_PAYLOAD}\"}}"

        print(f"JSON formatted payload: {MQTT_MSG}")

        


    if len(MQTT_MSG) > 8192:

        print("Cannot send MQTT message greater than 8KB. Please reduce the number of EPOCHS in configuration parameters to reduce the size.")

        exit()


    # this is our callback function when we receive a location on our subscribed topic

    def message_handler(client, userdata, message):

        json_resp = message.payload.decode('utf-8')

        print(f"received message: {json_resp}")

        print(f"message topic: {message.topic}")

        print(f"message QoS: {message.qos}")


        json_resp = json.loads(json_resp)

        lat = json_resp["Lat"]

        lon = json_resp["Lon"]

        print(f"I am here: https://www.google.com/maps/search/?api=1&query={lat},{lon}")

        # calling disconnect will cause blocking loop to exit, effectively ending the program

        client.disconnect()


    # configure MQTT client

    client = mqtt.Client(DeviceID)

    client.username_pw_set(username = Username, password = Password)

    client.connect(Hostname)

    client.on_message = message_handler

    # subscribe to topic

    if MQTT_SUB_TOPIC:

        client.subscribe(MQTT_SUB_TOPIC)


    # publish message

    client.publish(MQTT_PUB_TOPIC, MQTT_MSG)

    print(f"message published: {MQTT_MSG} ")

    # wait for a position back from CloudLocate

    # you will receive it in the callback function

    # once payload is received in callback, it will disconnect the client, which will cause the loop to break

    # hence ending the script

    client.loop_forever()