0
votes

I have working code that sends a struct from a Raspberry Pi to an Arduino using pySerialTransfer when the devices are connected via a USB cable. I'd like to do that over bluetooth instead of with a USB cable, however.

Independently, using separate python code and a separate Arduino sketch, I've been able to get the Raspberry Pi & Arduino to communicate over bluetooth via a continuous stream of text or binary data.

My issue is that I don't see how to combine the two approaches - i.e.: if I send a binary-encoded structure over bluetooth, I don't see how I get the benefits of the pySerialTransfer / SerialTransfer libraries to parse it on the receiving end. I see how I can do the parsing "manually", looking for special terminating characters, etc., but I was hoping to avoid the need for that with pySerialTransfer.

Thanks for any pointers / suggestions / examples. All the working code I've been able to construct so far is here.

pySerialTransfer

Working Arduino C serial code

#include "SerialTransfer.h"

SerialTransfer myTransfer;

int const ONBOARD_LED_PIN = 13;

struct POSITION {
  int id;
  float azimuth;
  float altitude;
} position;

void Blink(int n) {
  for (int i=0; i<n; i++) {
    digitalWrite(ONBOARD_LED_PIN, HIGH);
    delay(75);
    digitalWrite(ONBOARD_LED_PIN, LOW);
    delay(75);
  }
  delay(150);
}


void setup()
{
  Serial.begin(115200);
  myTransfer.begin(Serial);
  pinMode(ONBOARD_LED_PIN, OUTPUT);
  digitalWrite(ONBOARD_LED_PIN, LOW);
}

void loop()
{
  if(myTransfer.available())
  {
    //////////////////////////////////////////////
    // handle call from Python

    myTransfer.rxObj(position, sizeof(position));

    //////////////////////////////////////////////

    //////////////////////////////////////////////
    // send response
    myTransfer.txObj(position, sizeof(position));
    myTransfer.sendData(sizeof(position));
    //////////////////////////////////////////////
  }
  else if(myTransfer.status < 0)
  {
    Serial.print("ERROR: ");

    if(myTransfer.status == -1)
      Serial.println(F("CRC_ERROR"));
    else if(myTransfer.status == -2)
      Serial.println(F("PAYLOAD_ERROR"));
    else if(myTransfer.status == -3)
      Serial.println(F("STOP_BYTE_ERROR"));
  }
}

Working Raspberry Pi serial code

import time
import struct
from pySerialTransfer import pySerialTransfer


def StuffObject(txfer_obj, val, format_string, object_byte_size, start_pos=0):
  """Insert an object into pySerialtxfer TX buffer starting at the specified index.

  Args:
    txfer_obj: txfer - Transfer class instance to communicate over serial
    val: value to be inserted into TX buffer
    format_string: string used with struct.pack to pack the val
    object_byte_size: integer number of bytes of the object to pack
    start_pos: index of the last byte of the float in the TX buffer + 1

  Returns:
    start_pos for next object
  """
  val_bytes = struct.pack(format_string, *val)
  for index in range(object_byte_size):
    txfer_obj.txBuff[index + start_pos] = val_bytes[index]
  return object_byte_size + start_pos


if __name__ == '__main__':
  try:
    link = pySerialTransfer.SerialTransfer('/dev/cu.usbmodem14201', baud=115200)

    link.open()
    time.sleep(2) # allow some time for the Arduino to completely reset
    base = time.time()

    while True:

      sent = (4, 1.2, 2.5)
      format_string = '<lff'
      format_size = 4+4+4
      StuffObject(link, sent, format_string, format_size, start_pos=0)
      link.send(format_size)

      start_time = time.time()
      elapsed_time = 0
      while not link.available() and elapsed_time < 2:
        if link.status < 0:
          print('ERROR: {}'.format(link.status))
        else:
          print('.', end='')
        elapsed_time = time.time()-start_time
      print()

      response = bytearray(link.rxBuff[:link.bytesRead])
      response = struct.unpack(format_string, response)

      print('SENT: %s' % str(sent))
      print('RCVD: %s' % str(response))
      print(' ')

  except KeyboardInterrupt:
    link.close()

Bluetooth Communication

Working Arduino bluetooth communication

#include "SerialTransfer.h"
// Connect the HC-05 TX to Arduino pin 2 RX. 
// Connect the HC-05 RX to Arduino pin 3 TX through a voltage divider.
// 
 

long n = 0;
struct POSITION {
  float azimuth=5;
  float altitude=10;
};

SerialTransfer myTransfer;

void setup() 
{
    Serial.begin(9600);
 
    // HC-06 default serial speed for communcation mode is 9600
    Serial1.begin(9600);  
    myTransfer.begin(Serial1);
}
 
void loop() 
{
    n++;
    POSITION x;
    x.azimuth=n;
    x.altitude=n+1;
    
    myTransfer.txObj(x, sizeof(x));
    myTransfer.sendData(sizeof(x));
    
    if(Serial1.available() > 0){ // Checks whether data is comming from the serial port
      Serial.println(Serial1.read());} // Reads the data from the serial port
    delay(1000);
}

Working python bluetooth communication

import bluetooth
sock=bluetooth.BluetoothSocket( bluetooth.RFCOMM )
bd_addr = '98:D3:11:FC:42:16'
port = 1
sock.connect((bd_addr, port))
d = sock.recv(10240000)
print(d)
sock.send("hello")
2

2 Answers

0
votes

pySerialTransfer / SerialTransfer will arrange data in packet format with CRC so it fast and safe way to communicate over serial.

In fact, there are no special character to terminate packet because binary data can be any character from 0x00 to 0xFF cover all ASCII code.

0
votes

I could not get the socket connection to last more than a few hours (see Detecting and recovering from “hanging” bluetooth python connection ); perhaps my socket attempt was an incorrect or at least unnecessary combination of technologies given ultimate goal of using pySerialTransfer. Falling back to just serial, I ultimately was able to get this code working for bidirectional RFCOMM serial communication between my Raspberry Pi and Arduino over bluetooth.

Python Code

#!/usr/bin/python3

import datetime
import sys
import time
import subprocess

from pySerialTransfer import pySerialTransfer

COMMAND_START_CHAR = '<'
COMMAND_END_CHAR = '>'
LOGFILE = 'bt.log'


def Log(s):
  """Appends message s to the logfile."""
  with open(LOGFILE, 'a') as f:
    f.write('%s\n' % s)


def Time(epoch):
  """Converts epoch to easy-to-read string."""
  return datetime.datetime.fromtimestamp(epoch).strftime('%Y-%m-%d %H:%M:%S')


def ConnectBluetoothRetry(attempts=1):
  """Attempts to make connections for a number of attempts, exiting program on fail."""
  attempt = 1
  while attempt <= attempts:
    try:
      ser = pySerialTransfer.SerialTransfer('/dev/rfcomm1', 9600)
    except pySerialTransfer.InvalidSerialPort as e:
      Log('ERROR: Unbound; will attempt to bind: %s' % e)
      cmd = 'sudo rfcomm bind 1 98:D3:11:FC:42:16 1'
      conn = subprocess.Popen(cmd, shell=True)
      if conn.returncode is None:
        Log('ERROR: Unable to bind')
        sys.exit()
      else:
        Log('Connection bound with return code %s' % conn.returncode)
        if attempt == attempts:
          attempts += 1
          Log('Allowing one more attempt to connect')
    if ser:
      Log('Connected after %d attempts' % attempt)
      return ser
    attempt += 1
    if attempt < attempts:
      time.sleep(1)
  Log('ERROR: Failed to connect after %d attempts' % attempt)
  sys.exit()


def ReconnectOnError(ser, error=None):
  """Close and reopen the serial."""
  if error:
    Log('ERROR: %s' % error)
  ser.close()
  ser = ConnectBluetoothRetry(10)
  return ser


def Parse(s):
  """Returns the string encapsulated between COMMAND_START_CHAR and COMMAND_END_CHAR."""
  if COMMAND_START_CHAR in s:
    start_char = s.find(COMMAND_START_CHAR)
  else:
    return None
  if COMMAND_END_CHAR in s:
    end_char = s.rfind(COMMAND_END_CHAR)
  else:
    return None
  return s[start_char + len(COMMAND_START_CHAR): end_char - len(COMMAND_END_CHAR) + 1]


def Read(ser):
  """Non-blocking read on an open Bluetooth pySerialTransfer."""
  recv = ''
  if ser.available():
    if ser.status < 0:
      if ser.status == -1:
        Log('ERROR: CRC_ERROR')
      elif ser.status == -2:
        Log('ERROR: PAYLOAD_ERROR')
      elif ser.status == -3:
        Log('ERROR: STOP_BYTE_ERROR')
    else:
      for index in range(ser.bytesRead):
        recv += chr(ser.rxBuff[index])
  if recv:
    command = Parse(recv)
    if command is not None:
      command = recv[len(COMMAND_START_CHAR):-len(COMMAND_END_CHAR)]
      return command
    Log('ERROR: malformed %s' % recv)

  return None


def Write(ser, command):
  """Sends the encapsulated string command on an open Bluetooth pySerialTransfer."""
  send = COMMAND_START_CHAR+str(command)+COMMAND_END_CHAR
  byte_count = ser.tx_obj(send)
  ser.send(byte_count)
  Log('Sent %s' % send)


def main():
  """Sends sequential numbers over bluetooth, and receives & parses anything sent."""
  sys.stderr = open(LOGFILE, 'a')

  start = time.time()
  last_write = start
  Log('Started at %s' % Time(start))

  ser = ConnectBluetoothRetry(10)

  x = 0
  while True:
    try:
      command = Read(ser)
    except Exception as e:
      ser = ReconnectOnError(ser, 'Failed to receive: %s' % e)
    if command is not None:
      Log('Recd: %s; runtime: %.3f hours' % (command, (time.time() - start) / 60**2))

    if time.time() - last_write > 1:
      last_write = time.time()
      try:
        Write(ser, x)
      except Exception as e:
        ser = ReconnectOnError(ser, 'Failed to send: %s' % e)
      x += 1

    time.sleep(.1)


if __name__ == "__main__":
  main()

Arduino Code

#include "SerialTransfer.h"


// Connect the HC-05 TX to Arduino pin 2 RX. 
// Connect the HC-05 RX to Arduino pin 3 TX through a voltage divider.
// 

SerialTransfer myTransfer;

char number[12];

unsigned long n = 1;
long last_mesg_received = 0;
long last_mesg_sent = 0;

void(* resetFunc) (void) = 0; //declare reset function @ address 0

void setup() 
{
    Serial.begin(9600);
 
    // HC-05 default serial speed for communcation mode is 9600
    Serial1.begin(9600);
    myTransfer.begin(Serial1);
      
    last_mesg_received = millis();
}
 
void Read(){
  if(myTransfer.available())
  {
    Serial.print("Recd: ");
    for(byte i = 0; i < myTransfer.bytesRead; i++)
      Serial.write(myTransfer.rxBuff[i]);
    Serial.println();
  }
  else if(myTransfer.status < 0)
  {
    Serial.print("ERROR: ");

    if(myTransfer.status == -1)
      Serial.println(F("CRC_ERROR"));
    else if(myTransfer.status == -2)
      Serial.println(F("PAYLOAD_ERROR"));
    else if(myTransfer.status == -3)
      Serial.println(F("STOP_BYTE_ERROR"));
  }
}


void Write(){
  sprintf(number, "<%d>", n);
  int len = strlen(number) + 1;
  for(int x; x<len; x++){
    myTransfer.txBuff[x+1] = number[x];
  }
  Serial.print("Sent: ");
  Serial.println(number);
  
  
  myTransfer.sendData(len);
  
}

void loop() 
{
  Read();
  if (millis() - last_mesg_sent > 1000){
    Write();
    last_mesg_sent = millis();
     n++;    
  }
  
  delay(100);

}

This produces output like the following:

Python bt.log

Started at 2020-05-05 09:04:47
Connected after 1 attempts
Sent <0>
Sent <1>
Sent <2>
Recd: <1; runtime: 0.001 hours
Recd: <1; runtime: 0.001 hours
Recd: <2; runtime: 0.001 hours
Recd: <3; runtime: 0.001 hours
Sent <3>
Recd: <4; runtime: 0.001 hours
Sent <4>
Recd: <5; runtime: 0.002 hours
Sent <5>
Recd: <6; runtime: 0.002 hours
Sent <6>
Sent <7>
Recd: <7; runtime: 0.002 hours
Sent <8>
Recd: <8; runtime: 0.003 hours
Sent <9>
Recd: <9; runtime: 0.003 hours
Sent <10>
Recd: <10; runtime: 0.003 hours
Sent <11>
Recd: <11; runtime: 0.003 hours
Sent <12>
Recd: <12; runtime: 0.004 hours
Sent <13>
Recd: <13; runtime: 0.004 hours
Sent <14>
Recd: <14; runtime: 0.004 hours
Sent <15>
Recd: <15; runtime: 0.005 hours
Sent <16>
Recd: <16; runtime: 0.005 hours
Sent <17>

Arduino Serial Monitor

09:04:49.591 -> Sent: <1>
09:04:50.564 -> Sent: <2>
09:04:51.368 -> Recd: <0>
09:04:51.477 -> Recd: <1>
09:04:51.583 -> Recd: <2>
09:04:51.687 -> Sent: <3>
09:04:52.080 -> Recd: <3>
09:04:52.777 -> Sent: <4>
09:04:52.997 -> Recd: <4>
09:04:53.804 -> Sent: <5>
09:04:54.096 -> Recd: <5>
09:04:54.896 -> Sent: <6>
09:04:55.083 -> Recd: <6>
09:04:55.995 -> Sent: <7>
09:04:56.104 -> Recd: <7>
09:04:56.970 -> Sent: <8>
09:04:57.073 -> Recd: <8>
09:04:58.084 -> Recd: <9>
09:04:58.084 -> Sent: <9>
09:04:59.106 -> Recd: <10>
09:04:59.174 -> Sent: <10>
09:05:00.087 -> Recd: <11>
09:05:00.197 -> Sent: <11>
09:05:01.111 -> Recd: <12>
09:05:01.291 -> Sent: <12>
09:05:02.091 -> Recd: <13>
09:05:02.380 -> Sent: <13>
09:05:03.103 -> Recd: <14>
09:05:03.390 -> Sent: <14>
09:05:04.087 -> Recd: <15>
09:05:04.518 -> Sent: <15>
09:05:05.091 -> Recd: <16>
09:05:05.607 -> Sent: <16>