Geotab / android-external-device-example

Example project for interfacing with a USB-IOX using the Android Open Accessory protocol. The example displays streaming Hours Of Service data and allows saving of generic Status Data.
MIT License
7 stars 8 forks source link

How to send messages from MyGeotab to External device #5

Open spehj opened 1 month ago

spehj commented 1 month ago

I'd like to send messages from MyGeotab Runner to the external Android device connected to the Geotab GO device with the USB cable.

I use this API call with the Runner:

api.call("Add", {
    "typeName": "TextMessage",
    "entity": {
        "device": {"id":"b2"}, // Replace with device ID that should receive the data
        "messageContent": {
            "contentType": "MimeContent",
            "channelNumber": 1,
            "mimeType": "text", // Can be changed to any free format text value
            "binaryDataPacketDelay": "00:00:03.0000000", // Applies a configurable delay of up to 5 seconds in between each sequenced message of a multimessage MIME payload
            "data": "SGVsbG8gV29ybGQ=" // Replace with your data encoded in base64
        },
    "isDirectionToVehicle": true,
    "messageSize": 235 // If unspecified defaults to 235. Max of 1000.
    },
}, function(result) {
    console.log("Done: ", result);
}, function(e) {
    console.error("Failed:", e);
});

I'm using your example code, where I only changed the Device ID to a number between 4200 and 4299 based on your documentation.

My ThirdParty.java code:

public class ThirdParty {
private static final int DEVICE_ID = 4208; // Used in methods sendHandshakeConfirmation and BuildHandshakeMessage

// Some other code ...

 // State machine to handle the third party protocol - See case SEND_CONFIRMATION
    private class StateMachine implements Runnable {
        private State eState = State.SEND_SYNC;
        private AtomicBoolean fRunning = new AtomicBoolean(true);

        public void run() {
            Log.i(TAG, "Third party SM started");

            while (fRunning.get()) {
                mLock.lock();        // The lock is needed for await and atomic access to flags/buffers

                try {
                    notifyStateChanged(eState);
                    switch (eState) {
                        case SEND_SYNC: {
                            byte[] abMessage = new byte[]{MESSAGE_SYNC};
                            mAccessoryControl.write(abMessage);
                            eState = State.WAIT_FOR_HANDSHAKE;
                            Log.d("THIRD_PARTY", "SEND_SYNC");
                            break;
                        }
                        case WAIT_FOR_HANDSHAKE: {
                            // Waits for the handshake message or resends sync every 1s
                            mEvent.await(1000, TimeUnit.MILLISECONDS);

                            if (mfHandshakeReceived) {
                                eState = State.SEND_CONFIRMATION;
                            } else {
                                eState = State.SEND_SYNC;
                            }
                            break;
                        }
                        case SEND_CONFIRMATION: {
                            sendHandshakeConfirmation(); // Added this (see below)
                            // Could also use this without flags:
                            // byte[] abMessage = BuildHandshakeMessage();
                            // mAccessoryControl.write(abMessage);
                            eState = State.PRE_IDLE;
                            break;
                        }
                        case PRE_IDLE: {
                            mfHandshakeReceived = false;
                            mfAckReceived = false;
                            mfMessageToSend = false;
                            eState = State.IDLE;
                            break;
                        }
                        case IDLE: {
                            // Sleep and wait for a handshake or a message to send
                            mEvent.await();

                            if (mfHandshakeReceived) {
                                eState = State.SEND_CONFIRMATION;
                            } else if (mfMessageToSend) {
                                mAccessoryControl.write(mabMessage);
                                eState = State.WAIT_FOR_ACK;
                            }
                            break;
                        }
                        case WAIT_FOR_ACK: {
                            // Wait for the ack or reset after 5s
                            mEvent.await(5000, TimeUnit.MILLISECONDS);

                            if (mfAckReceived) {
                                eState = State.PRE_IDLE;
                            } else {
                                eState = State.SEND_SYNC;
                            }
                            break;
                        }
                        default: {
                            eState = State.SEND_SYNC;
                            break;
                        }
                    }

                } catch (InterruptedException e) {
                    Log.w(TAG, "Exception during await", e);
                } finally {
                    mLock.unlock();
                }
            }
        }

        // Stop the thread
        public void close() {
            Log.i(TAG, "Shutting down third party SM");

            mLock.lock();
            try {
                fRunning.set(false);
                mfHandshakeReceived = false;
                mfAckReceived = false;
                mfMessageToSend = false;
                mEvent.signal();
            } finally {
                mLock.unlock();
            }
        }
    }

// Some other code ...

    // Wrote this to send a confirmation with the flags as stated in the documentation
    public void sendHandshakeConfirmation() {
        byte[] handshakeMessage = BuildHandshakeConfirmationMessage();
        Log.d(TAG, "Sending Handshake Confirmation: " + Arrays.toString(handshakeMessage));
        mAccessoryControl.write(handshakeMessage);
    }

    private byte[] BuildHandshakeConfirmationMessage() {
        byte[] abMessage = new byte[10];

        // Start of Text
        abMessage[0] = 0x02;

        // Message Type
        abMessage[1] = MESSAGE_CONFIRMATION;

        // Message Body Length
        abMessage[2] = 4;

        // External Device ID (little-endian)
        abMessage[3] = (byte) (DEVICE_ID & 0xFF);
        abMessage[4] = (byte) ((DEVICE_ID >> 8) & 0xFF);

        // Flags
        byte flags = 0x06; // Handshake ACK = 1, Binary data wrapping = 1, Self-powered = 0
        abMessage[5] = flags;

        // Checksum (Fletcher's checksum)
        byte[] abChecksum = CalcChecksum(abMessage, 6);
        abMessage[6] = abChecksum[0];
        abMessage[7] = abChecksum[1];

        // End of Text
        abMessage[8] = 0x03;

        return abMessage;
    }

// Some other code ...

// I also tried this method without the flags that uses existing BuildMessage method
// Assemble the handshake message with the device ID - could be called in 
    private byte[] BuildHandshakeMessage() {
        byte[] abDeviceId = new byte[]{
                (byte) (DEVICE_ID & 0xFF),
                (byte) ((DEVICE_ID >> 8) & 0xFF),
                0x00, 0x00 // Placeholder for any additional data
        };
        return BuildMessage(MESSAGE_CONFIRMATION, abDeviceId);
    }

}

My procedure:

  1. I press Run in the Geotab Runner, the message displayed in the console is Done:b54, Done:b55 (number increasing for each call).
  2. I watch for any raw bytes coming to my Android device inside the RxMessage method in ThirdParty.java, but nothing gets printed.

Example:

public void RxMessage(byte[] abData) {
        Log.d(TAG, "RxMessage: abData.length:" + abData.length + ", abData:" + Arrays.toString(abData));
 // Other code ....       
}

How to make this work? Am I missing something?

spehj commented 1 month ago

Additional information with logs for 3 cases:

  1. Normal BuildMessage method (creates confirmation for HOS_ENHANCED_ID_WITH_ACK message) - device ID 4141 (0x2D, 0x10 in bytes)
  2. Confirmation sending device ID with flags as per documentation (binary data wrapping etc) - BuildHandshakeConfirmationMessageWithFlags method with device ID between 4200 and 4299 (4208 in this case)
  3. Confirmation sending device ID without flags - BuildHandshakeMessageWithoutFlags method with device ID between 4200 and 4299 (4208 in this case)

Modified ThirdParty.java

I modified the ThirdParty.java a bit to test all 3 cases.

I created two methods to test cases 2 and 3: BuildHandshakeConfirmationMessageWithFlags and BuildHandshakeMessageWithoutFlags. Full modified ThirdParty.java file content:

/*****************************************************************************
 *
 * Copyright (C) 2017, Geotab Inc.
 *
 ******************************************************************************
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *****************************************************************************/
package com.geotab.AOA;

import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.atomic.AtomicBoolean;

import android.content.Context;
import android.os.Handler;
import android.util.Log;

import com.geotab.ioxproto.IoxMessaging;
import com.google.protobuf.InvalidProtocolBufferException;

public class ThirdParty {
    private static final String TAG = ThirdParty.class.getSimpleName();    // Used for error logging

    private static final HOSData mHOSdata = new HOSData();

    private static final byte MESSAGE_HANDSHAKE = 1;
    private static final byte MESSAGE_ACK = 2;
    private static final byte MESSAGE_GO_DEVICE_DATA = 0x21;
    private static final byte PROTOBUF_DATA_FROM_GO = 0x26;
    private static final byte MESSAGE_CONFIRMATION = (byte) 0x81;
    private static final byte CUSTOM_IOX = (byte) 0x1D;
    private static final byte MESSAGE_STATUS_DATA = (byte) 0x80;
    private static final byte TP_FREE_FORMAT_DATA = (byte) 0x82;
    private static final byte TP_DEVICE_INFO_RECEIVED = (byte) 0x83;
    private static final byte TP_HOS_ACK = (byte) 0x84;
    static final byte PROTOBUF_DATA_TO_GO = (byte) 0x8c;
    private static final byte MESSAGE_SYNC = 0x55;
    private static final byte[] HOS_ENHANCED_ID_WITH_ACK = new byte[]{0x2D, 0x10, 0x00, 0x00}; //
    // private static final byte[] HOS_ENHANCED_ID_WITH_ACK = new byte[]{0x60, 0x10, 0x00, 0x00};
    // private static final int DEVICE_ID = 4141; // device ID that works
    private static final int DEVICE_ID = 4208; // this device ID does not work

    static final ThirdPartyMessage[] THIRD_PARTY_MESSAGE_DEFINEs = new ThirdPartyMessage[]
            {
                    new ThirdPartyMessage("-BYPASS-", (byte) 0, null),
                    new ThirdPartyMessage("STATUS: OUTSIDE TEMPERATURE", MESSAGE_STATUS_DATA, new byte[]{0x35, 0x00}),        // 53
                    new ThirdPartyMessage("STATUS: ENGINE WARNING LIGHT", MESSAGE_STATUS_DATA, new byte[]{0x24, 0x00}),        // 36
                    new ThirdPartyMessage("STATUS: PARK BRAKE", MESSAGE_STATUS_DATA, new byte[]{0x31, 0x00}),                // 49
                    new ThirdPartyMessage("FREE FORMAT", TP_FREE_FORMAT_DATA, null),
                    new ThirdPartyMessage("DEVICE INFO", TP_DEVICE_INFO_RECEIVED, null),
                    new ThirdPartyMessage("HOS ACK", TP_HOS_ACK, null),
                    new ThirdPartyMessage("PROTOBUF PUB/SUB", PROTOBUF_DATA_TO_GO, null),
            };

    private final Lock mLock = new ReentrantLock();
    private final Condition mEvent = mLock.newCondition();

    private byte[] mabMessage;
    private boolean mfAckReceived, mfHandshakeReceived, mfMessageToSend;

    private AccessoryControl mAccessoryControl;
    private StateMachine mStateMachine;
    private final Handler mHandler;
    private IOXListener mIOXListener;

    public enum State {
        SEND_SYNC, WAIT_FOR_HANDSHAKE, SEND_CONFIRMATION, PRE_IDLE, IDLE, WAIT_FOR_ACK
    }

    // Constructor
    public ThirdParty(AccessoryControl accessory, Context context, IOXListener ioxListener) {
        mfHandshakeReceived = false;
        mfAckReceived = false;
        mfMessageToSend = false;
        mAccessoryControl = accessory;
        mHandler = new Handler(context.getMainLooper());
        mIOXListener = ioxListener;
        mStateMachine = new StateMachine();
        new Thread(mStateMachine).start();        // Run as a separate thread
    }

    // State machine to handle the third party protocol
    private class StateMachine implements Runnable {
        private State eState = State.SEND_SYNC;
        private AtomicBoolean fRunning = new AtomicBoolean(true);

        public void run() {
            Log.i(TAG, "Third party SM started");

            while (fRunning.get()) {
                mLock.lock();        // The lock is needed for await and atomic access to flags/buffers

                try {
                    notifyStateChanged(eState);
                    switch (eState) {
                        case SEND_SYNC: {
                            byte[] abMessage = new byte[]{MESSAGE_SYNC};
                            mAccessoryControl.write(abMessage);
                            eState = State.WAIT_FOR_HANDSHAKE;
                            Log.d("THIRD_PARTY", "SEND_SYNC");
                            break;
                        }
                        case WAIT_FOR_HANDSHAKE: {
                            // Waits for the handshake message or resends sync every 1s
                            mEvent.await(1000, TimeUnit.MILLISECONDS);

                            if (mfHandshakeReceived) {
                                eState = State.SEND_CONFIRMATION;
                            } else {
                                eState = State.SEND_SYNC;
                            }
                            break;
                        }
                        case SEND_CONFIRMATION: {
                            /// Test with HOS_ENHANCED_ID_WITH_ACK
                            // byte[] abMessage = BuildMessage(MESSAGE_CONFIRMATION, HOS_ENHANCED_ID_WITH_ACK); // Uncomment this
                            /// Test with flags
                            // byte[] abMessage = BuildHandshakeConfirmationMessageWithFlags(); // Uncomment this
                            /// Test without flags
                             byte[] abMessage = BuildHandshakeMessageWithoutFlags(); // Uncomment this
                            Log.d(TAG, "SEND_CONFIRMATION: abMessage.length:" + abMessage.length + ", abMessage:" + Arrays.toString(abMessage));
                            mAccessoryControl.write(abMessage);
                            eState = State.PRE_IDLE;
                            break;
                        }
                        case PRE_IDLE: {
                            mfHandshakeReceived = false;
                            mfAckReceived = false;
                            mfMessageToSend = false;
                            eState = State.IDLE;
                            break;
                        }
                        case IDLE: {
                            // Sleep and wait for a handshake or a message to send
                            mEvent.await();

                            if (mfHandshakeReceived) {
                                eState = State.SEND_CONFIRMATION;
                            } else if (mfMessageToSend) {
                                mAccessoryControl.write(mabMessage);
                                eState = State.WAIT_FOR_ACK;
                            }
                            break;
                        }
                        case WAIT_FOR_ACK: {
                            // Wait for the ack or reset after 5s
                            mEvent.await(5000, TimeUnit.MILLISECONDS);

                            if (mfAckReceived) {
                                eState = State.PRE_IDLE;
                            } else {
                                eState = State.SEND_SYNC;
                            }
                            break;
                        }
                        default: {
                            eState = State.SEND_SYNC;
                            break;
                        }
                    }

                } catch (InterruptedException e) {
                    Log.w(TAG, "Exception during await", e);
                } finally {
                    mLock.unlock();
                }
            }
        }

        // Stop the thread
        public void close() {
            Log.i(TAG, "Shutting down third party SM");

            mLock.lock();
            try {
                fRunning.set(false);
                mfHandshakeReceived = false;
                mfAckReceived = false;
                mfMessageToSend = false;
                mEvent.signal();
            } finally {
                mLock.unlock();
            }
        }
    }

    // Signal the state machine to stop
    public void close() {
        if (mStateMachine != null)
            mStateMachine.close();
    }

    // Encapsulate a message to be sent
    public void TxMessage(byte bType, byte[] abData) {
        Log.d(TAG, "TxMessage:" + bType + ", abData.length:" + abData.length);
        mLock.lock();
        try {
            mabMessage = BuildMessage(bType, abData);
            Log.d(TAG, "TxMessage: mabMessage.length:" + mabMessage.length + ", mabMessage:" + Arrays.toString(mabMessage));
            mfMessageToSend = true;
            mEvent.signal();
        } finally {
            mLock.unlock();
        }
    }

    // Checks if a received message matches the expected third party format
    public void RxMessage(byte[] abData) {
        Log.d(TAG, "RxMessage: abData.length:" + abData.length + ", abData:" + Arrays.toString(abData));

        // Check length
        if (abData == null || abData.length < 6) {
            Log.e(TAG, "RxMessage: Bad Data length!");
            return;
        }

        // Check structure
        byte bSTX = abData[0];
        byte bLength = abData[2];
        byte bETX = abData[abData.length - 1];

        if (bSTX != 0x02 || bETX != 0x03) {
            Log.e(TAG, "RxMessage: Bad Data format!");
            return;
        }

        // Check checksum
        byte[] abChecksum = new byte[]{abData[abData.length - 3], abData[abData.length - 2]};
        byte[] abCalcChecksum = CalcChecksum(abData, bLength + 3);

        if (!Arrays.equals(abChecksum, abCalcChecksum)) {
            Log.e(TAG, "RxMessage: Bad Data Checksum!");
            return;
        }

        byte bType = abData[1];

        switch (bType) {
            case MESSAGE_HANDSHAKE:
                mLock.lock();
                try {
                    mfHandshakeReceived = true;
                    mEvent.signal();
                } finally {
                    mLock.unlock();
                }
                break;

            case MESSAGE_ACK:
                mLock.lock();
                try {
                    mfAckReceived = true;
                    mEvent.signal();
                } finally {
                    mLock.unlock();
                }
                break;

            case MESSAGE_GO_DEVICE_DATA:
                ExtractHOSData(abData);

                byte[] abAck = new byte[]{};
                mabMessage = BuildMessage(TP_HOS_ACK, abAck);
                mAccessoryControl.write(mabMessage);
                break;
            case PROTOBUF_DATA_FROM_GO:
                try {
                    byte[] mDate = new byte[abData.length - 6];
                    System.arraycopy(abData, 3, mDate, 0, mDate.length);
                    IoxMessaging.IoxFromGo ioxFromGoMsg = IoxMessaging.IoxFromGo.parseFrom(mDate);
                    Log.d(TAG, "RxMessage: PROTOBUF_DATA_FROM_GO MsgCase:"
                            + ioxFromGoMsg.getMsgCase());
                    if (mIOXListener != null && mHandler != null) {
                        mHandler.post(() -> {
                            mIOXListener.onIOXReceived(ioxFromGoMsg);
                        });
                    }
                } catch (InvalidProtocolBufferException e) {
                    Log.e(TAG, "RxMessage: Failed to decode the protobuf data\n"
                            + e.getMessage());
                }
                break;

            // Case some other message type
            default:
                Log.d(TAG, "RxMessage: Unknown message type: " + bType);
                break;
        }
    }

    // Assemble a third party message
    private byte[] BuildMessage(byte bType, byte[] abData) {
        byte[] abMessage = new byte[abData.length + 6];

        abMessage[0] = 0x02;
        abMessage[1] = bType;
        abMessage[2] = (byte) abData.length;

        System.arraycopy(abData, 0, abMessage, 3, abData.length);

        int iLengthUpToChecksum = abData.length + 3;
        byte[] abCalcChecksum = CalcChecksum(abMessage, iLengthUpToChecksum);
        System.arraycopy(abCalcChecksum, 0, abMessage, iLengthUpToChecksum, 2);

        abMessage[abMessage.length - 1] = 0x03;

        return abMessage;
    }

    private byte[] BuildHandshakeConfirmationMessageWithFlags() {
        byte[] abMessage = new byte[10];

        // Start of Text
        abMessage[0] = 0x02;

        // Message Type
        abMessage[1] = MESSAGE_CONFIRMATION;

        // Message Body Length
        abMessage[2] = 4;

        // External Device ID (little-endian)
        abMessage[3] = (byte) (DEVICE_ID & 0xFF);
        abMessage[4] = (byte) ((DEVICE_ID >> 8) & 0xFF);

        // Flags
        byte flags = 0x06; // Handshake ACK = 1, Binary data wrapping = 1, Self-powered = 0
        abMessage[5] = flags;

        // Checksum (Fletcher's checksum)
        byte[] abChecksum = CalcChecksum(abMessage, 6);
        abMessage[6] = abChecksum[0];
        abMessage[7] = abChecksum[1];

        // End of Text
        abMessage[8] = 0x03;

        return abMessage;
    }

    // Assemble the handshake message with device ID and without flags
    private byte[] BuildHandshakeMessageWithoutFlags(){
        byte[] abDeviceId = new byte[]{
                (byte) ((DEVICE_ID >> 8) & 0xFF),
                (byte) (DEVICE_ID & 0xFF),
                0x00, 0x00
        };
        return BuildMessage(MESSAGE_CONFIRMATION, abDeviceId);
    }

    // Calculate the Fletcher's checksum over the given bytes
    private byte[] CalcChecksum(byte[] abData, int iLength) {
        byte[] abChecksum = new byte[]{0x00, 0x00};

        for (int i = 0; i < iLength; i++) {
            abChecksum[0] += abData[i];
            abChecksum[1] += abChecksum[0];
        }

        return abChecksum;
    }

    public void ExtractHOSData(byte[] abData) {
        synchronized (mHOSdata) {
            Log.d("NewTest", "ExtractHOSData: " + abData.length);
            ByteBuffer abConvert;

            byte[] abDateTime = new byte[4];
            System.arraycopy(abData, 3, abDateTime, 0, abDateTime.length);
            abConvert = ByteBuffer.wrap(abDateTime).order(java.nio.ByteOrder.LITTLE_ENDIAN);
            int iDateTime = abConvert.getInt();
            Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
            c.clear();
            c.set(2002, Calendar.JANUARY, 1);        // (Units given in seconds since Jan 1, 2002)
            c.add(Calendar.SECOND, iDateTime);
            SimpleDateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z", Locale.US);
            mHOSdata.sDateTime = dataFormat.format(c.getTime());

            byte[] abLatitude = new byte[4];
            System.arraycopy(abData, 7, abLatitude, 0, abLatitude.length);
            abConvert = ByteBuffer.wrap(abLatitude).order(java.nio.ByteOrder.LITTLE_ENDIAN);
            int iLatitude = abConvert.getInt();
            mHOSdata.Latitude = (float) iLatitude / 10000000;    // (Units given in 10^-7)

            byte[] abLogitude = new byte[4];
            System.arraycopy(abData, 11, abLogitude, 0, abLogitude.length);
            abConvert = ByteBuffer.wrap(abLogitude).order(java.nio.ByteOrder.LITTLE_ENDIAN);
            int iLogitude = abConvert.getInt();
            mHOSdata.Longitude = (float) iLogitude / 10000000;    // (Units given in 10^-7)

            mHOSdata.iRoadSpeed = abData[15];

            byte[] abPRM = new byte[2];
            System.arraycopy(abData, 16, abPRM, 0, abPRM.length);
            abConvert = ByteBuffer.wrap(abPRM).order(java.nio.ByteOrder.LITTLE_ENDIAN);
            mHOSdata.iRPM = abConvert.getShort();
            mHOSdata.iRPM /= 4;            // Convert to RPM (Units given in 0.25)

            byte[] abOdometer = new byte[4];
            System.arraycopy(abData, 18, abOdometer, 0, abOdometer.length);
            abConvert = ByteBuffer.wrap(abOdometer).order(java.nio.ByteOrder.LITTLE_ENDIAN);
            mHOSdata.iOdometer = abConvert.getInt();    // (Units given in 0.1/km)

            byte bStatus = abData[22];
            mHOSdata.sStatus = "";

            if ((bStatus & (1 << 0)) != 0)
                mHOSdata.sStatus += "GPS Latched | ";
            else
                mHOSdata.sStatus += "GPS Invalid | ";

            if ((bStatus & (1 << 1)) != 0)
                mHOSdata.sStatus += "IGN on | ";
            else
                mHOSdata.sStatus += "IGN off | ";

            if ((bStatus & (1 << 2)) != 0)
                mHOSdata.sStatus += "Engine Data | ";
            else
                mHOSdata.sStatus += "No Engine Data | ";

            if ((bStatus & (1 << 3)) != 0)
                mHOSdata.sStatus += "Date/Time Valid | ";
            else
                mHOSdata.sStatus += "Date/Time Invalid | ";

            if ((bStatus & (1 << 4)) != 0)
                mHOSdata.sStatus += "Speed From Engine | ";
            else
                mHOSdata.sStatus += "Speed From GPS | ";

            if ((bStatus & (1 << 5)) != 0)
                mHOSdata.sStatus += "Distance From Engine | ";
            else
                mHOSdata.sStatus += "Distance From GPS | ";

            byte[] abTripOdometer = new byte[4];
            System.arraycopy(abData, 23, abTripOdometer, 0, abTripOdometer.length);
            abConvert = ByteBuffer.wrap(abTripOdometer).order(java.nio.ByteOrder.LITTLE_ENDIAN);
            mHOSdata.iTripOdometer = abConvert.getInt();    // (Units given in 0.1/km)

            byte[] abEngineHours = new byte[4];
            System.arraycopy(abData, 27, abEngineHours, 0, abEngineHours.length);
            abConvert = ByteBuffer.wrap(abEngineHours).order(java.nio.ByteOrder.LITTLE_ENDIAN);
            mHOSdata.iEngineHours = abConvert.getInt();        // Already in units of 0.1h

            byte[] abTripDuration = new byte[4];
            System.arraycopy(abData, 31, abTripDuration, 0, abTripDuration.length);
            abConvert = ByteBuffer.wrap(abTripDuration).order(java.nio.ByteOrder.LITTLE_ENDIAN);
            mHOSdata.iTripDuration = abConvert.getInt();        // Units of seconds

            byte[] abVehicleId = new byte[4];
            System.arraycopy(abData, 35, abVehicleId, 0, abVehicleId.length);
            abConvert = ByteBuffer.wrap(abVehicleId).order(java.nio.ByteOrder.LITTLE_ENDIAN);
            mHOSdata.iVehicleId = abConvert.getInt();

            byte[] abDriverId = new byte[4];
            System.arraycopy(abData, 39, abDriverId, 0, abDriverId.length);
            abConvert = ByteBuffer.wrap(abDriverId).order(java.nio.ByteOrder.LITTLE_ENDIAN);
            mHOSdata.iDriverId = abConvert.getInt();
        }

        updateHOSTextFromThread();
    }

    public synchronized HOSData getHOSData() {
        return mHOSdata;
    }

    // Update text on the UI thread from another thread
    private void updateHOSTextFromThread() {
        HOSData dataHOS = getHOSData();
        if (mHandler != null && mIOXListener != null) {
            mHandler.post(() -> mIOXListener.onUpdateHOSText(dataHOS));
        }
    }

    private void showStatusMsg(final String msg) {
        Log.i(TAG, msg);
        if (mHandler != null && mIOXListener != null) {
            mHandler.post(() -> mIOXListener.onStatusUpdate(msg));
        }
    }

    private void notifyStateChanged(ThirdParty.State state) {
        Log.i(TAG, "notifyStateChanged " + state.name());
        if (mHandler != null && mIOXListener != null) {
            mHandler.post(() -> mIOXListener.onIOXStateChanged(state));
        }
    }
}

Logs

Logs for case 1:

2024-08-01 08:45:37.240 12918-12963 ThirdParty              com.geotab.AOA                       I  Third party SM started
2024-08-01 08:45:37.241 12918-12963 ThirdParty              com.geotab.AOA                       I  notifyStateChanged SEND_SYNC
2024-08-01 08:45:37.245 12918-12963 ThirdParty              com.geotab.AOA                       I  notifyStateChanged WAIT_FOR_HANDSHAKE
2024-08-01 08:45:37.246 12918-12964 ThirdParty              com.geotab.AOA                       D  RxMessage: abData.length:58, abData:[2, 33, 52, 12, 51, 122, 42, 0, -110, 59, 27, 0, 93, -97, 8, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, 71, 65, 85, 54, 66, 69, 85, 69, 80, 50, 90, 50, 118, -40, 3]
2024-08-01 08:45:37.353 12918-12964 ThirdParty              com.geotab.AOA                       D  RxMessage: abData.length:6, abData:[2, 1, 0, 3, 8, 3]
2024-08-01 08:45:37.355 12918-12963 ThirdParty              com.geotab.AOA                       I  notifyStateChanged SEND_CONFIRMATION
2024-08-01 08:45:37.362 12918-12963 ThirdParty              com.geotab.AOA                       D  SEND_CONFIRMATION: abMessage.length:10, abMessage:[2, -127, 4, 45, 16, 0, 0, -60, 12, 3]
2024-08-01 08:45:37.363 12918-12963 ThirdParty              com.geotab.AOA                       I  notifyStateChanged PRE_IDLE
2024-08-01 08:45:37.366 12918-12963 ThirdParty              com.geotab.AOA                       I  notifyStateChanged IDLE
2024-08-01 08:45:39.377 12918-12964 ThirdParty              com.geotab.AOA                       D  RxMessage: abData.length:58, abData:[2, 33, 52, 19, 51, 122, 42, 0, -110, 59, 27, 0, 93, -97, 8, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 7, 8, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, 71, 65, 85, 54, 66, 69, 85, 69, 80, 50, 90, 50, -124, -20, 3]
2024-08-01 08:45:41.376 12918-12964 ThirdParty              com.geotab.AOA                       D  RxMessage: abData.length:58, abData:[2, 33, 52, 21, 51, 122, 42, 0, -110, 59, 27, 0, 93, -97, 8, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 9, 8, 0, 0, -1, -1, -1, -1, 0, 0, 0, 0, 71, 65, 85, 54, 66, 69, 85, 69, 80, 50, 90, 50, -120, -124, 3]

HOS data is being receved as expected.

  1. Logs for case 2:
    2024-08-01 08:47:40.879 13622-13672 ThirdParty              com.geotab.AOA                       I  Third party SM started
    2024-08-01 08:47:40.879 13622-13672 ThirdParty              com.geotab.AOA                       I  notifyStateChanged SEND_SYNC
    2024-08-01 08:47:40.882 13622-13672 ThirdParty              com.geotab.AOA                       I  notifyStateChanged WAIT_FOR_HANDSHAKE
    2024-08-01 08:47:40.883 13622-13673 ThirdParty              com.geotab.AOA                       D  RxMessage: abData.length:6, abData:[2, 1, 0, 3, 8, 3]
    2024-08-01 08:47:40.884 13622-13672 ThirdParty              com.geotab.AOA                       I  notifyStateChanged SEND_CONFIRMATION
    2024-08-01 08:47:40.885 13622-13672 ThirdParty              com.geotab.AOA                       D  SEND_CONFIRMATION: abMessage.length:10, abMessage:[2, -127, 4, 112, 16, 6, 13, 23, 3, 0]
    2024-08-01 08:47:40.886 13622-13672 ThirdParty              com.geotab.AOA                       I  notifyStateChanged PRE_IDLE
    2024-08-01 08:47:40.886 13622-13672 ThirdParty              com.geotab.AOA                       I  notifyStateChanged IDLE
    2024-08-01 08:48:23.408 13622-13673 ThirdParty              com.geotab.AOA                       I  Shutting down third party SM

No data is being received from the device after the confirmation sent from the Android device.

I tried sending the Runner's data, but nothing was logged.

  1. Logs for case 3:
    2024-08-01 08:51:33.701 15060-15107 ThirdParty              com.geotab.AOA                       I  Third party SM started
    2024-08-01 08:51:33.701 15060-15107 ThirdParty              com.geotab.AOA                       I  notifyStateChanged SEND_SYNC
    2024-08-01 08:51:33.702 15060-15107 ThirdParty              com.geotab.AOA                       I  notifyStateChanged WAIT_FOR_HANDSHAKE
    2024-08-01 08:51:33.704 15060-15108 ThirdParty              com.geotab.AOA                       D  RxMessage: abData.length:6, abData:[2, 1, 0, 3, 8, 3]
    2024-08-01 08:51:33.706 15060-15107 ThirdParty              com.geotab.AOA                       I  notifyStateChanged SEND_CONFIRMATION
    2024-08-01 08:51:33.707 15060-15107 ThirdParty              com.geotab.AOA                       D  SEND_CONFIRMATION: abMessage.length:10, abMessage:[2, -127, 4, 16, 112, 0, 0, 7, -72, 3]
    2024-08-01 08:51:33.708 15060-15107 ThirdParty              com.geotab.AOA                       I  notifyStateChanged PRE_IDLE
    2024-08-01 08:51:33.709 15060-15107 ThirdParty              com.geotab.AOA                       I  notifyStateChanged IDLE
    2024-08-01 08:57:32.009 15060-15060 ThirdParty              com.geotab.AOA                       I  Shutting down third party SM

Same as case 2 - no data is being received from the device after the confirmation sent from the Android device.

I tried sending the Runner's data, but nothing was logged.

For cases 2 and 3 I used this API call to get data from the Runner to external Android device:

api.call("Add", {
    "typeName": "TextMessage",
    "entity": {
        "device": {"id":"b1"}, // Replace with device ID that should receive the data
        "messageContent": {
            "contentType": "MimeContent",
            "channelNumber": 5,
            "mimeType": "text", // Can be changed to any free format text value
            "binaryDataPacketDelay": "00:00:03.0000000", // Applies a configurable delay of up to 5 seconds in between each sequenced message of a multimessage MIME payload
            "data": "SGVsbG8gV29ybGQ=" // Replace with your data encoded in base64
        },
    "isDirectionToVehicle": true,
    "messageSize": 235 // If unspecified defaults to 235. Max of 1000.
    },
}, function(result) {
    console.log("Done: ", result);
}, function(e) {
    console.error("Failed:", e);
});