pythonreact-nativebase64bluetooth-lowenergy

Raspberry Pi not decoding base 64 encoded data sent over BLE using Nordic UART service from a React Native app


I'm using a react native app to write to and read from a .ini file on a Raspberry Pi, which I call a hub, which runs a GATT server using the Nordic UART service (NUS).

I'm able to connect to the RPi and send a string, "retrieveHubConfig", which is supposed to read the current .ini file contents and display them in the app. I also sends JSON data that can be written to the .ini file automatically, without needing the user of the app to enter it. The rest of the .ini file is populated from data the user enters and is written to the RPi in the same way as before.

The Raspberry Pi receives base 64 encoded data and prints it out before and after being decoded.

However the decoding process doesn't seem to be working. It prints out a byte array of the JSON data and the "retrieveHubConfig" string in up to 100 character chunks. There's also an end of transmission character sent after the complete JSON data is sent, and another one for "retrieveHubConfig". The except branch of the try except block in the GATT server prints:

(first 100 characters of JSON data)

Error in WriteValue: Incorrect padding

(characters 101 - 200 of JSON data)

Error in WriteValue: 'utf-8' codec can't decode byte 0xdb in position 0: invalid continuation byte

(last 12 characters of JSON data)

Error in WriteValue: 'utf-8' codec can't decode byte 0xdf in position 0: invalid continuation byte

(all 17 characters of retrieveHubConfig)

Error in WriteValue: Invalid base64-encoded string: number of data characters (17) cannot be 1 more than a multiple of 4

Interestingly, it manages to execute the "Received base64 decoded data:" print statement for the end of transmission characters, but it's a blank string.

I get the same output from the Pi if I use:

const paddedData = command.padEnd(Math.ceil(command.length / 4) * 4, "=");

to try and resolve the incorrect padding problem.

I've tried using the buffer and react-native-base64 libraries. They both produce the same output from the Pi. I expect to see the JSON data:

{"hub_settings": {"hello_timer": "3000", "inter-master_multicast_address": "244.0.0.221", "neighbour_inter-master_multicast_address": "244.0.0.222"}}

and: "retrieveHubConfig"

in the "Received base64 decoded data:" print statement.

Here's the output of the Raspberry Pi GATT server:

Received data:  dbus.Array([dbus.Byte(123), dbus.Byte(34), dbus.Byte(104), dbus.Byte(117), dbus.Byte(98), dbus.Byte(83), dbus.Byte(101), dbus.Byte(116), dbus.Byte(116), dbus.Byte(105), dbus.Byte(110), dbus.Byte(103), dbus.Byte(115), dbus.Byte(34), dbus.Byte(58), dbus.Byte(123), dbus.Byte(34), dbus.Byte(104), dbus.Byte(117), dbus.Byte(98), dbus.Byte(95), dbus.Byte(115), dbus.Byte(101), dbus.Byte(116), dbus.Byte(116), dbus.Byte(105), dbus.Byte(110), dbus.Byte(103), dbus.Byte(115), dbus.Byte(34), dbus.Byte(58), dbus.Byte(123), dbus.Byte(34), dbus.Byte(105), dbus.Byte(110), dbus.Byte(116), dbus.Byte(101), dbus.Byte(114), dbus.Byte(45), dbus.Byte(109), dbus.Byte(97), dbus.Byte(115), dbus.Byte(116), dbus.Byte(101), dbus.Byte(114), dbus.Byte(95), dbus.Byte(109), dbus.Byte(117), dbus.Byte(108), dbus.Byte(116), dbus.Byte(105), dbus.Byte(99), dbus.Byte(97), dbus.Byte(115), dbus.Byte(116), dbus.Byte(95), dbus.Byte(97), dbus.Byte(100), dbus.Byte(100), dbus.Byte(114), dbus.Byte(101), dbus.Byte(115), dbus.Byte(115), dbus.Byte(34), dbus.Byte(58), dbus.Byte(34), dbus.Byte(50), dbus.Byte(52), dbus.Byte(52), dbus.Byte(46), dbus.Byte(48), dbus.Byte(46), dbus.Byte(48), dbus.Byte(46), dbus.Byte(50)], signature=dbus.Signature('y'))
Error in WriteValue: Incorrect padding
Received data:  dbus.Array([dbus.Byte(50), dbus.Byte(49), dbus.Byte(34), dbus.Byte(44), dbus.Byte(34), dbus.Byte(110), dbus.Byte(101), dbus.Byte(105), dbus.Byte(103), dbus.Byte(104), dbus.Byte(98), dbus.Byte(111), dbus.Byte(117), dbus.Byte(114), dbus.Byte(95), dbus.Byte(105), dbus.Byte(110), dbus.Byte(116), dbus.Byte(101), dbus.Byte(114), dbus.Byte(45), dbus.Byte(109), dbus.Byte(97), dbus.Byte(115), dbus.Byte(116), dbus.Byte(101), dbus.Byte(114), dbus.Byte(95), dbus.Byte(109), dbus.Byte(117), dbus.Byte(108), dbus.Byte(116), dbus.Byte(105), dbus.Byte(99), dbus.Byte(97), dbus.Byte(115), dbus.Byte(116), dbus.Byte(95), dbus.Byte(97), dbus.Byte(100), dbus.Byte(100), dbus.Byte(114), dbus.Byte(101), dbus.Byte(115), dbus.Byte(115), dbus.Byte(34), dbus.Byte(58), dbus.Byte(34), dbus.Byte(50), dbus.Byte(52), dbus.Byte(52), dbus.Byte(46), dbus.Byte(48), dbus.Byte(46), dbus.Byte(48), dbus.Byte(46), dbus.Byte(50), dbus.Byte(50), dbus.Byte(50), dbus.Byte(34), dbus.Byte(44), dbus.Byte(34), dbus.Byte(104), dbus.Byte(101), dbus.Byte(108), dbus.Byte(108), dbus.Byte(111), dbus.Byte(95), dbus.Byte(116), dbus.Byte(105), dbus.Byte(109), dbus.Byte(101), dbus.Byte(114), dbus.Byte(34), dbus.Byte(58)], signature=dbus.Signature('y'))
Error in WriteValue: 'utf-8' codec can't decode byte 0xdb in position 0: invalid continuation byte
Received data:  dbus.Array([dbus.Byte(34), dbus.Byte(51), dbus.Byte(48), dbus.Byte(48), dbus.Byte(48), dbus.Byte(34), dbus.Byte(125), dbus.Byte(125), dbus.Byte(125)], signature=dbus.Signature('y'))
Error in WriteValue: 'utf-8' codec can't decode byte 0xdf in position 0: invalid continuation byte
Received data:  dbus.Array([dbus.Byte(4)], signature=dbus.Signature('y'))
Received base64 decoded data:  
Received data:  dbus.Array([dbus.Byte(114), dbus.Byte(101), dbus.Byte(116), dbus.Byte(114), dbus.Byte(105), dbus.Byte(101), dbus.Byte(118), dbus.Byte(101), dbus.Byte(72), dbus.Byte(117), dbus.Byte(98), dbus.Byte(67), dbus.Byte(111), dbus.Byte(110), dbus.Byte(102), dbus.Byte(105), dbus.Byte(103)], signature=dbus.Signature('y'))
Error in WriteValue: Invalid base64-encoded string: number of data characters (17) cannot be 1 more than a multiple of 4
Received data:  dbus.Array([dbus.Byte(4)], signature=dbus.Signature('y'))
Received base64 decoded data:

The react native code. The getHubConfig() method contains the encoding and writing to Raspberry Pi over BLE statements:

import base64 from "react-native-base64";
import { Context } from "../Auth/AuthContext.js";

// Method that presents the user with a list of BLE hubs or watches to choose from and allows
// them to connect to and transfer data between this app and the RPi.

const BleScanner = ({ engineerMode, deviceType }) => {
    const globalContext = useContext(Context);
    const { selectedDevice, setSelectedDevice } = globalContext;

    // UUIDs for the UART service and characteristics. These are used to send the commands to the RPi.
    // The RPi will respond with the result of the command.They are standard Nordic UART UUIDs.
    const UART_SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
    const UART_RX_CHARACTERISTIC_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
    const UART_TX_CHARACTERISTIC_UUID = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";

    // Set the MTU to the desired maximum value
    const MTU = 100;
    //End - of - Transmission marker, ASCII code for EOT
    const EOT_MARKER = "\x04";

    const [devices, setDevices] = useState([]);
    const [isConnecting, setIsConnecting] = useState(false);
    // New state to track if the app is connecting to the RPi or watch
    const [showParameters, setShowParameters] = useState(false);
    // New state to track if the parameters of the selected hub or watch are visible

    const getHubConfig = async () => {
        setShowParameters(false);
        setIsConnecting(true);

        const hubSettings = {"hub_settings": {"hello_timer": "3000", "inter-master_multicast_address": "244.0.0.221", "neighbour_inter-master_multicast_address": "244.0.0.222"}}

        const commands = [
            JSON.stringify({ hubSettings }),
            "retrieveHubConfig",
        ];

        try {
            // Check if RPi is still connected to the device or not. If it is not connected, then connect to the device.
            let connectedDevice = await handleDeviceConnection(selectedDevice);

            const service = await connectedDevice.discoverAllServicesAndCharacteristics();
            const characteristics = await service.characteristicsForService(UART_SERVICE_UUID);
            const rx_characteristic = characteristics.find((c) => c.uuid === UART_RX_CHARACTERISTIC_UUID);

            for (const command of commands) {
                let data = null;
                console.log("Command: ", command);
                if (command === "retrieveHubConfig") {
                    // const paddedData = command.padEnd(Math.ceil(command.length / 4) * 4, "=");
                    data = base64.encode(command);
                } else {
                    // const paddedData = command.padEnd(Math.ceil(command.length / 4) * 4, "=");
                    data = base64.encode(command);
                }
                console.log("Data to send: ", data);
                console.log("Decoded data to send: ", Buffer.from(data, "base64").toString("ascii"));

                // Calculate the number of packets needed to send the data
                const numPackets = Math.ceil(data.length / MTU);
                console.log("Number of packets: ", numPackets); // Log the number of packets
                // Send the data in smaller units
                for (let i = 0; i < numPackets; i++) {
                    const start = i * MTU;
                    const end = Math.min(start + MTU, data.length);
                    const packetData = data.slice(start, end);
                    console.log("Packet data: ", packetData);
                    console.log("Packet data length: ", packetData.length);

                    // Send the packet to the connected device
                    await rx_characteristic.writeWithoutResponse(packetData);

                    console.log(`Packet ${i + 1}/${numPackets} sent.`);

                    // Pause for 0.5 seconds between packets.
                    await new Promise(resolve => setTimeout(resolve, 1000));

                    if (i == numPackets - 1) {
                        await rx_characteristic.writeWithoutResponse(Buffer.from(EOT_MARKER).toString("base64"));
                        console.log("EOT length: ", Buffer.from(EOT_MARKER).toString("base64").length);
                    }
                }

                console.log("Data transmission complete.");
            }
            saveExtracedData(extractKeyValuePairs(readCharacteristic(connectedDevice)));
            setShowParameters(true);
            setIsConnecting(false);
        } catch (error) {
            // If an error occurs, log the error and set the isConnecting state variable to false
            console.log("Failed to send commands: ", error);
            console.log("Error code: ", error.errorCode);

            setIsConnecting(false);
            Alert.alert("Couldn't connect.",
                Platform.OS === "ios" ?
                    "Check your device is paired with Tycho hub in your device's bluetooth settings. Stay close to Tycho hub." : "Stay close to Tycho hub & retry.");
        }
    };

    const readCharacteristic = (device) => {
        try {
            let isEOTReceived = false;
            let fullData = "";

            while (!isEOTReceived) {
                console.log("Reading characteristic...");
                const characteristic = manager.readCharacteristicForDevice(
                    device.id,
                    UART_SERVICE_UUID,
                    UART_TX_CHARACTERISTIC_UUID
                );
                console.log("Characteristic: ", characteristic);
                const data = characteristic.value;
                console.log("Received data:", data);

                const decodedData = Buffer.from(data, "base64").toString("ascii");
                // Check for EOT packet. If EOT packet is received, then stop reading the characteristic.
                if (decodedData.includes(EOT_MARKER)) {
                    isEOTReceived = true;
                    fullData += decodedData; // Add decoded data to fullData
                    break;
                }

                fullData += decodedData;

                // Optional: Handle the case where data might exceed 100 bytes per packet
                if (data.length > MTU) {
                    console.warn(`Received data packet exceeds ${MTU} bytes`);
                }
            }

            console.log("Received full data:", fullData);
            return fullData;
        } catch (error) {
            console.error("Read characteristic error:", error);
            throw error; // Rethrow the error for further handling if necessary
        }
    };
};

export default BleScanner;

The Raspberry Pi Python code. The RxCharacteristic class' WriteValue() method contains the print and base 64 decoding statements mentioned above:

import gi

gi.require_version('DBus', '1.0')
from gi.repository import GLib
from gi.repository import Gio
from gi.repository import DBus

import os
import time
import json
import base64
import sys
import subprocess
import configparser
import dbus
import dbus.mainloop.glib
from ble_advertisement import Advertisement
from ble_advertisement import register_ad_cb, register_ad_error_cb
from ble_gatt_server import Service, Characteristic
from ble_gatt_server import register_app_cb, register_app_error_cb

BLUEZ_SERVICE_NAME = 'org.bluez'
DBUS_OM_IFACE = 'org.freedesktop.DBus.ObjectManager'
LE_ADVERTISING_MANAGER_IFACE = 'org.bluez.LEAdvertisingManager1'
GATT_MANAGER_IFACE = 'org.bluez.GattManager1'
GATT_CHRC_IFACE = 'org.bluez.GattCharacteristic1'
UART_SERVICE_UUID = '6e400001-b5a3-f393-e0a9-e50e24dcca9e'
UART_RX_CHARACTERISTIC_UUID = '6e400002-b5a3-f393-e0a9-e50e24dcca9e'
UART_TX_CHARACTERISTIC_UUID = '6e400003-b5a3-f393-e0a9-e50e24dcca9e'
LOCAL_NAME = 'Tycho-Hub-1'
mainloop = None

PACKET_SIZE = 100
EOT_MARKER = "\x04"  # End-of-Transmission marker, ASCII code for EOT
received_data = []  # Store received data packets

class CasePreservingConfigParser(configparser.ConfigParser):
    def optionxform(self, optionstr):
        return optionstr

class TxCharacteristic(Characteristic):
    def __init__(self, bus, index, service):
        Characteristic.__init__(self, bus, index, UART_TX_CHARACTERISTIC_UUID, ['read'], service)
        GLib.io_add_watch(sys.stdin, GLib.IO_IN, self.on_console_input)
        self.isReadable = True

    def on_console_input(self, fd, condition):
        s = fd.readline()
        if s.isspace():
            pass
        else:
            self.send_tx(s)
        return True

    def send_tx(self, config_dict):
        global tx_data_packets
        config_json = json.dumps(config_dict, indent=4)
        print(f"Sending config JSON: {config_json}")
        total_length = len(config_json)
        print(f"Total Length of Data: {total_length} bytes")
        # Split data into packets and store in the global list
        tx_data_packets = [base64.b64encode(config_json[i:i + PACKET_SIZE].encode()).decode() for i in range(0, total_length, PACKET_SIZE)]
        tx_data_packets.append(base64.b64encode(EOT_MARKER.encode()).decode())  # Append base64 encoded EOT marker to the end of the list
        print(f"Number of Packets Prepared: {len(tx_data_packets)}")

    def ReadValue(self, options):
        global tx_data_packets
        if not tx_data_packets:
            return GLib.Variant('ay', [])  # Return empty if no more packets

        # Pop the first packet from the list and return it
        packet = tx_data_packets.pop(0)
        print(f"Sending packet: {packet}")
        return GLib.Variant('ay', packet)

    def StartNotify(self):
        if self.notifying:
            return
        self.notifying = True

    def StopNotify(self):
        if not self.notifying:
            return
        self.notifying = False

class RxCharacteristic(Characteristic):
    def __init__(self, bus, index, service):
        Characteristic.__init__(self, bus, index, UART_RX_CHARACTERISTIC_UUID, ['write'], service)

    def WriteValue(self, value, options):
        try:
            print("Received data: ", value)
            print("Received data length: ", len(value))
            # Convert the received data to a byte array
            byte_array = bytearray(value)
            # Decode the received base64 data
            value_str = base64.b64decode(byte_array).decode('utf-8')
            print("Received base64 decoded data: ", value_str)
            received_data.append(value_str)  # Append received data to the buffer

            # Check for EOT (End of Transmission) marker
            if EOT_MARKER in value_str:
                received_data_str = ''.join(received_data).split(EOT_MARKER)[0]  # Split at EOT and take first part
                self.execute_command(received_data_str)  # Execute the command with all received data
                received_data.clear()  # Clear the buffer
        except Exception as e:
            print(f"Error in WriteValue: {e}")

    def execute_command(self, command):
        """
        Executes the received command. If the command is "retrieveHubConfig"
        then send_hub_config() is called, otherwise if it contains 'cloud_settings'
        or 'hub_settings' in its JSON structure, it calls update_hub_config().
        Otherwise, executes the command as a shell command.
        """
        print(f"Received Command: '{command}'")

        if "retrieveHubConfig" in command:
            self.send_hub_config()
        else:
            try:
                command_json = json.loads(command)
                if 'cloud_settings' in command_json or 'hub_settings' in command_json:
                    self.update_hub_config(command)
            except json.JSONDecodeError:
                # Not a JSON command, execute as shell command
                self.execute_shell_command(command)

    def execute_shell_command(self, command):
        """
        Executes a command in the shell.
        """
        try:
            output = subprocess.check_output(command, shell=True)
            print("Shell Command Output:", output.decode())
        except subprocess.CalledProcessError as e:
            print(f"Error executing shell command: {e}")

    def send_hub_config(self):
        # Read cloud settings from hubConfig.ini using the case-preserving parser
        config = CasePreservingConfigParser()
        config.read(os.path.join(os.path.dirname(__file__), 'hubConfig.ini'))

        config_dict = {}
        # Iterate over sections and their options
        for section in config.sections():
            config_dict[section] = {}
            for option in config.options(section):
                config_dict[section][option] = config.get(section, option)

        print("Sending hub config...")
        self.service.tx_characteristic.send_tx(config_dict)

    def update_hub_config(self, settings):
        print("In update hub config method")
        print(f"Settings: {settings}")

        try:
            # Parse the JSON string into a dictionary
            settings_dict = json.loads(settings)
            print(f"Settings dictionary: {settings_dict}")

            # Create a CasePreservingConfigParser object and read the existing INI file
            config = CasePreservingConfigParser()
            config.read("hubConfig.ini")

            # Iterate over the dictionary and update the ConfigParser object
            for section, options in settings_dict.items():
                if not config.has_section(section):
                    config.add_section(section)

                for key, value in options.items():
                    config.set(section, key, str(value))

            print(f"Config parser object after update: {config}")

            # Write the updated configuration back to the INI file
            with open("hubConfig.ini", "w") as configfile:
                config.write(configfile)

            print("Updated hubConfig.ini with JSON data")

        except json.JSONDecodeError as e:
            print(f"Error decoding JSON: {e}")
        except configparser.Error as e:
            print(f"ConfigParser error: {e}")
        except Exception as e:
            print(f"General error in update_hub_config: {e}")

class UartService(Service):
    def __init__(self, bus, index):
        Service.__init__(self, bus, index, UART_SERVICE_UUID, True)
        self.tx_characteristic = TxCharacteristic(bus, 0, self)
        self.rx_characteristic = RxCharacteristic(bus, 1, self)
        self.add_characteristic(self.tx_characteristic)
        self.add_characteristic(self.rx_characteristic)

class Application(dbus.service.Object):
    def __init__(self, bus):
        self.path = '/'
        self.services = []
        dbus.service.Object.__init__(self, bus, self.path)

    def get_path(self):
        return dbus.ObjectPath(self.path)

    def add_service(self, service):
        self.services.append(service)

    @dbus.service.method(DBUS_OM_IFACE, out_signature='a{oa{sa{sv}}}')
    def GetManagedObjects(self):
        response = {}
        for service in self.services:
            response[service.get_path()] = service.get_properties()
            chrcs = service.get_characteristics()
            for chrc in chrcs:
                response[chrc.get_path()] = chrc.get_properties()
        return response

class UartApplication(Application):
    def __init__(self, bus):
        Application.__init__(self, bus)
        self.add_service(UartService(bus, 0))

class UartAdvertisement(Advertisement):
    def __init__(self, bus, index):
        Advertisement.__init__(self, bus, index, 'peripheral')
        self.add_service_uuid(UART_SERVICE_UUID)
        self.add_local_name(LOCAL_NAME)
        self.include_tx_power = True

def find_adapter(bus):
    remote_om = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, '/'),
                               DBUS_OM_IFACE)
    objects = remote_om.GetManagedObjects()
    for o, props in objects.items():
        if LE_ADVERTISING_MANAGER_IFACE in props and GATT_MANAGER_IFACE in props:
            return o
        print('Skip adapter:', o)
    return None

def main():
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    bus = dbus.SystemBus()
    adapter = find_adapter(bus)
    if not adapter:
        print('BLE adapter not found')
        return

    service_manager = dbus.Interface(
        bus.get_object(BLUEZ_SERVICE_NAME, adapter),
        GATT_MANAGER_IFACE)
    ad_manager = dbus.Interface(bus.get_object(BLUEZ_SERVICE_NAME, adapter),
                                LE_ADVERTISING_MANAGER_IFACE)

    app = UartApplication(bus)
    adv = UartAdvertisement(bus, 0)

    mainloop = GLib.MainLoop()

    service_manager.RegisterApplication(app.get_path(), {},
                                        reply_handler=register_app_cb,
                                        error_handler=register_app_error_cb)
    ad_manager.RegisterAdvertisement(adv.get_path(), {},
                                     reply_handler=register_ad_cb,
                                     error_handler=register_ad_error_cb)

    try:
        mainloop.run()
    except KeyboardInterrupt:
        adv.Release()

if __name__ == '__main__':
    main()

Solution

  • This is the experiment I did to prove the data being received was not in base64:

    import dbus
    
    
    transmissions = [
        dbus.Array(
            [
                dbus.Byte(123),
                dbus.Byte(34),
                dbus.Byte(104),
                dbus.Byte(117),
                dbus.Byte(98),
                dbus.Byte(83),
                dbus.Byte(101),
                dbus.Byte(116),
                dbus.Byte(116),
                dbus.Byte(105),
                dbus.Byte(110),
                dbus.Byte(103),
                dbus.Byte(115),
                dbus.Byte(34),
                dbus.Byte(58),
                dbus.Byte(123),
                dbus.Byte(34),
                dbus.Byte(104),
                dbus.Byte(117),
                dbus.Byte(98),
                dbus.Byte(95),
                dbus.Byte(115),
                dbus.Byte(101),
                dbus.Byte(116),
                dbus.Byte(116),
                dbus.Byte(105),
                dbus.Byte(110),
                dbus.Byte(103),
                dbus.Byte(115),
                dbus.Byte(34),
                dbus.Byte(58),
                dbus.Byte(123),
                dbus.Byte(34),
                dbus.Byte(105),
                dbus.Byte(110),
                dbus.Byte(116),
                dbus.Byte(101),
                dbus.Byte(114),
                dbus.Byte(45),
                dbus.Byte(109),
                dbus.Byte(97),
                dbus.Byte(115),
                dbus.Byte(116),
                dbus.Byte(101),
                dbus.Byte(114),
                dbus.Byte(95),
                dbus.Byte(109),
                dbus.Byte(117),
                dbus.Byte(108),
                dbus.Byte(116),
                dbus.Byte(105),
                dbus.Byte(99),
                dbus.Byte(97),
                dbus.Byte(115),
                dbus.Byte(116),
                dbus.Byte(95),
                dbus.Byte(97),
                dbus.Byte(100),
                dbus.Byte(100),
                dbus.Byte(114),
                dbus.Byte(101),
                dbus.Byte(115),
                dbus.Byte(115),
                dbus.Byte(34),
                dbus.Byte(58),
                dbus.Byte(34),
                dbus.Byte(50),
                dbus.Byte(52),
                dbus.Byte(52),
                dbus.Byte(46),
                dbus.Byte(48),
                dbus.Byte(46),
                dbus.Byte(48),
                dbus.Byte(46),
                dbus.Byte(50),
            ],
            signature=dbus.Signature("y"),
        ),
        dbus.Array(
            [
                dbus.Byte(50),
                dbus.Byte(49),
                dbus.Byte(34),
                dbus.Byte(44),
                dbus.Byte(34),
                dbus.Byte(110),
                dbus.Byte(101),
                dbus.Byte(105),
                dbus.Byte(103),
                dbus.Byte(104),
                dbus.Byte(98),
                dbus.Byte(111),
                dbus.Byte(117),
                dbus.Byte(114),
                dbus.Byte(95),
                dbus.Byte(105),
                dbus.Byte(110),
                dbus.Byte(116),
                dbus.Byte(101),
                dbus.Byte(114),
                dbus.Byte(45),
                dbus.Byte(109),
                dbus.Byte(97),
                dbus.Byte(115),
                dbus.Byte(116),
                dbus.Byte(101),
                dbus.Byte(114),
                dbus.Byte(95),
                dbus.Byte(109),
                dbus.Byte(117),
                dbus.Byte(108),
                dbus.Byte(116),
                dbus.Byte(105),
                dbus.Byte(99),
                dbus.Byte(97),
                dbus.Byte(115),
                dbus.Byte(116),
                dbus.Byte(95),
                dbus.Byte(97),
                dbus.Byte(100),
                dbus.Byte(100),
                dbus.Byte(114),
                dbus.Byte(101),
                dbus.Byte(115),
                dbus.Byte(115),
                dbus.Byte(34),
                dbus.Byte(58),
                dbus.Byte(34),
                dbus.Byte(50),
                dbus.Byte(52),
                dbus.Byte(52),
                dbus.Byte(46),
                dbus.Byte(48),
                dbus.Byte(46),
                dbus.Byte(48),
                dbus.Byte(46),
                dbus.Byte(50),
                dbus.Byte(50),
                dbus.Byte(50),
                dbus.Byte(34),
                dbus.Byte(44),
                dbus.Byte(34),
                dbus.Byte(104),
                dbus.Byte(101),
                dbus.Byte(108),
                dbus.Byte(108),
                dbus.Byte(111),
                dbus.Byte(95),
                dbus.Byte(116),
                dbus.Byte(105),
                dbus.Byte(109),
                dbus.Byte(101),
                dbus.Byte(114),
                dbus.Byte(34),
                dbus.Byte(58),
            ],
            signature=dbus.Signature("y"),
        ),
        dbus.Array(
            [
                dbus.Byte(34),
                dbus.Byte(51),
                dbus.Byte(48),
                dbus.Byte(48),
                dbus.Byte(48),
                dbus.Byte(34),
                dbus.Byte(125),
                dbus.Byte(125),
                dbus.Byte(125),
            ],
            signature=dbus.Signature("y"),
        ),
        dbus.Array([dbus.Byte(4)], signature=dbus.Signature("y")),
        dbus.Array(
            [
                dbus.Byte(114),
                dbus.Byte(101),
                dbus.Byte(116),
                dbus.Byte(114),
                dbus.Byte(105),
                dbus.Byte(101),
                dbus.Byte(118),
                dbus.Byte(101),
                dbus.Byte(72),
                dbus.Byte(117),
                dbus.Byte(98),
                dbus.Byte(67),
                dbus.Byte(111),
                dbus.Byte(110),
                dbus.Byte(102),
                dbus.Byte(105),
                dbus.Byte(103),
            ],
            signature=dbus.Signature("y"),
        ),
        dbus.Array([dbus.Byte(4)], signature=dbus.Signature("y")),
    ]
    
    data = []
    for transmission in transmissions:
        if bytes(transmission) == b'\x04':
            print("Data received before EOT")
            print("\t", "".join(data))
            data = []
        else:
            data.append(bytes(transmission).decode('utf8'))
    

    Which gave the following output:

    Data received before EOT
         {"hubSettings":{"hub_settings":{"inter-master_multicast_address":"244.0.0.221","neighbour_inter-master_multicast_address":"244.0.0.222","hello_timer":"3000"}}}
    Data received before EOT
         retrieveHubConfig