Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interface improvements #123

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 82 additions & 78 deletions brping/pingmessage.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import struct
from brping import definitions
payload_dict = definitions.payload_dict_all
asciiMsgs = [definitions.COMMON_NACK, definitions.COMMON_ASCII_TEXT]
variable_msgs = [definitions.PING1D_PROFILE, definitions.PING360_DEVICE_DATA, ]
asciiMsgs = [definitions.CommonMessage.NACK, definitions.CommonMessage.ASCII_TEXT]
variable_msgs = [definitions.Ping1dMessage.PROFILE, definitions.Ping360Message.DEVICE_DATA, ]


class PingMessage(object):
Expand Down Expand Up @@ -41,7 +41,8 @@ class PingMessage(object):
## number of bytes in a checksum
checksumLength = 2

## Messge constructor
## Message constructor
# Initialize from provided data (for packing and transmitting)
#
# @par Ex request:
# @code
Expand All @@ -53,74 +54,77 @@ class PingMessage(object):
#
# @par Ex set:
# @code
# m = PingMessage(PING1D_SET_RANGE)
# m.start_mm = 1000
# m.length_mm = 2000
# m.update_checksum()
# m = PingMessage(Ping1dMessage.SET_RANGE, start_mm=1000, length_mm=2000)
# m.pack_msg_data()
# write(m.msg_data)
# @endcode
#
# @par Ex receive:
# @code
# m = PingMessage(rxByteArray)
# if m.message_id == PING1D_RANGE
# start_mm = m.start_mm
# length_mm = m.length_mm
# @endcode
def __init__(self, msg_id=0, msg_data=None):
def __init__(self, msg_id=0, dst_device_id=0, src_device_id=0, **payload_data):
## The message id
self.message_id = msg_id

## The request id for request messages
self.request_id = None

## The message destination
self.dst_device_id = 0
self.dst_device_id = dst_device_id
## The message source
self.src_device_id = 0
self.src_device_id = src_device_id
## The message checksum
self.checksum = 0

## The raw data buffer for this message
# update with pack_msg_data()
self.msg_data = None

# Constructor 1: make a pingmessage object from a binary data buffer
# (for receiving + unpacking)
if msg_data is not None:
if not self.unpack_msg_data(msg_data):
# Attempted to create an unknown message
return
# Constructor 2: make a pingmessage object cooresponding to a message
# id, with field members ready to access and populate
# (for packing + transmitting)
else:
try:
## The name of this message
self.name = payload_dict[self.message_id].name

try:
## The name of this message
self.name = payload_dict[self.message_id]["name"]
## The field names of this message
self.payload_field_names = payload_dict[self.message_id].field_names

# initialize payload field members
for attr in self.payload_field_names:
setattr(self, attr, payload_data.get(attr, 0))

## The field names of this message
self.payload_field_names = payload_dict[self.message_id]["field_names"]
# initialize vector field if present in message
if self.message_id in variable_msgs:
last_field = self.payload_field_names[-1]
# only set if not already set by user
if getattr(self, last_field) == 0:
setattr(self, last_field, bytearray())

# initialize payload field members
for attr in self.payload_field_names:
setattr(self, attr, 0)
## Number of bytes in the message payload
self.update_payload_length()

# initialize vector fields
if self.message_id in variable_msgs:
setattr(self, self.payload_field_names[-1], bytearray())
## The struct formatting string for the message payload
self.update_payload_format()

## Number of bytes in the message payload
self.update_payload_length()
# TODO pack_msg_data
# Either when data is provided, or with a 'pack' argument (default True?)
# - avoid re-updating payload length

## The struct formatting string for the message payload
self.payload_format = self.get_payload_format()
except KeyError as e:
message_id = self.message_id
raise Exception(f"{message_id = } not recognized\n{msg_data = }") from e
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f-string f'{var=}' syntax requires Python >= 3.8
-> should specify in commit message
-> should update minimum version in setup.py

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can update to 1.* version, do all the necessary syntax corrections and move to python 3.9


# TODO handle better here, and catch Constructor 1 also
except KeyError as e:
print("message id not recognized: %d" % self.message_id, msg_data)
raise e
## Alternate constructor
# Initialize from a binary data buffer
#
# @par Ex receive:
# @code
# m = PingMessage.from_buffer(rxByteArray)
# if m.message_id == Ping1dMessage.RANGE
# start_mm = m.start_mm
# length_mm = m.length_mm
# @endcode
@classmethod
def from_buffer(cls, msg_data):
msg = cls()
if not msg.unpack_msg_data(msg_data):
# Attempted to create an unknown message
return
return msg

## Pack object attributes into self.msg_data (bytearray)
# @return self.msg_data
Expand All @@ -130,10 +134,11 @@ def pack_msg_data(self):
self.update_payload_length()

# Prepare struct packing format string
msg_format = PingMessage.endianess + PingMessage.header_format + self.get_payload_format()
self.update_payload_format()
msg_format = self.endianess + self.header_format + self.payload_format

# Prepare complete list of field names (header + payload)
attrs = PingMessage.header_field_names + payload_dict[self.message_id]["field_names"]
attrs = self.header_field_names + payload_dict[self.message_id].field_names

# Prepare iterable ordered list of values to pack
values = []
Expand All @@ -148,7 +153,7 @@ def pack_msg_data(self):
self.msg_data = bytearray(struct.pack(msg_format, *values))

# Update and append checksum
self.msg_data += bytearray(struct.pack(PingMessage.endianess + PingMessage.checksum_format, self.update_checksum()))
self.msg_data += bytearray(struct.pack(self.endianess + self.checksum_format, self.update_checksum()))

return self.msg_data

Expand All @@ -158,32 +163,32 @@ def unpack_msg_data(self, msg_data):
self.msg_data = msg_data

# Extract header
header = struct.unpack(PingMessage.endianess + PingMessage.header_format, self.msg_data[0:PingMessage.headerLength])
header = struct.unpack(self.endianess + self.header_format, self.msg_data[0:self.headerLength])

for i, attr in enumerate(PingMessage.header_field_names):
for i, attr in enumerate(self.header_field_names):
setattr(self, attr, header[i])

## The name of this message
try:
self.name = payload_dict[self.message_id]["name"]
self.name = payload_dict[self.message_id].name
except KeyError:
print("Unknown message: ", self.message_id)
return False

## The field names of this message
self.payload_field_names = payload_dict[self.message_id]["field_names"]
self.payload_field_names = payload_dict[self.message_id].field_names

if self.payload_length > 0:
## The struct formatting string for the message payload
self.payload_format = self.get_payload_format()
self.update_payload_format()

# Extract payload
try:
payload = struct.unpack(PingMessage.endianess + self.payload_format, self.msg_data[PingMessage.headerLength:PingMessage.headerLength + self.payload_length])
payload = struct.unpack(self.endianess + self.payload_format, self.msg_data[self.headerLength:self.headerLength + self.payload_length])
except Exception as e:
print("error unpacking payload: %s" % e)
print("msg_data: %s, header: %s" % (msg_data, header))
print("format: %s, buf: %s" % (PingMessage.endianess + self.payload_format, self.msg_data[PingMessage.headerLength:PingMessage.headerLength + self.payload_length]))
print("format: %s, buf: %s" % (self.endianess + self.payload_format, self.msg_data[self.headerLength:self.headerLength + self.payload_length]))
print(self.payload_format)
else: # only use payload if didn't raise exception
for i, attr in enumerate(self.payload_field_names):
Expand All @@ -196,12 +201,12 @@ def unpack_msg_data(self, msg_data):
pass

# Extract checksum
self.checksum = struct.unpack(PingMessage.endianess + PingMessage.checksum_format, self.msg_data[PingMessage.headerLength + self.payload_length: PingMessage.headerLength + self.payload_length + PingMessage.checksumLength])[0]
self.checksum = struct.unpack(self.endianess + self.checksum_format, self.msg_data[self.headerLength + self.payload_length: self.headerLength + self.payload_length + self.checksumLength])[0]
return True

## Calculate the checksum from the internal bytearray self.msg_data
def calculate_checksum(self):
return sum(self.msg_data[0:PingMessage.headerLength + self.payload_length]) & 0xffff
return sum(self.msg_data[0:self.headerLength + self.payload_length]) & 0xffff

## Update the object checksum value
# @return the object checksum value
Expand All @@ -215,30 +220,29 @@ def verify_checksum(self):

## Update the payload_length attribute with the **current** payload length, including dynamic length fields (if present)
def update_payload_length(self):
self.payload_length = payload_dict[self.message_id].payload_length

if self.message_id in variable_msgs or self.message_id in asciiMsgs:
# The last field self.payload_field_names[-1] is always the single dynamic-length field
self.payload_length = payload_dict[self.message_id]["payload_length"] + len(getattr(self, self.payload_field_names[-1]))
else:
self.payload_length = payload_dict[self.message_id]["payload_length"]
self.payload_length += len(getattr(self, self.payload_field_names[-1]))

## Get the python struct formatting string for the message payload
# @return the payload struct format string
def get_payload_format(self):
## Update the python struct formatting string for the message payload
def update_payload_format(self):
# messages with variable length fields
if self.message_id in variable_msgs or self.message_id in asciiMsgs:
var_length = self.payload_length - payload_dict[self.message_id]["payload_length"] # Subtract static length portion from payload length
if var_length <= 0:
return payload_dict[self.message_id]["format"] # variable data portion is empty

return payload_dict[self.message_id]["format"] + str(var_length) + "s"
else: # messages with a static (constant) length
return payload_dict[self.message_id]["format"]
# Subtract static length portion from payload length
var_length = self.payload_length - payload_dict[self.message_id].payload_length
if var_length > 0:
self.payload_format = payload_dict[self.message_id].format + str(var_length) + "s"
return
# messages with a static (constant) length, or empty data portion
self.payload_format = payload_dict[self.message_id].format

## Dump object into string representation
# @return string representation of the object
def __repr__(self):
header_string = "Header:"
for attr in PingMessage.header_field_names:
for attr in self.header_field_names:
header_string += " " + attr + ": " + str(getattr(self, attr))

if self.payload_length == 0: # this is a hack/guard for empty body requests
Expand All @@ -250,17 +254,17 @@ def __repr__(self):
if self.message_id in variable_msgs:

# static fields are handled as usual
for attr in payload_dict[self.message_id]["field_names"][:-1]:
for attr in payload_dict[self.message_id].field_names[:-1]:
payload_string += "\n - " + attr + ": " + str(getattr(self, attr))

# the variable length field is always the last field
attr = payload_dict[self.message_id]["field_names"][-1:][0]
attr = payload_dict[self.message_id].field_names[-1:][0]

# format this field as a list of hex values (rather than a string if we did not perform this handling)
payload_string += "\n - " + attr + ": " + str([hex(item) for item in getattr(self, attr)])

else: # handling of static length messages and text messages
for attr in payload_dict[self.message_id]["field_names"]:
for attr in payload_dict[self.message_id].field_names:
payload_string += "\n - " + attr + ": " + str(getattr(self, attr))

representation = (
Expand Down Expand Up @@ -369,7 +373,7 @@ def wait_checksum_h(self, msg_byte):
self.message_id = 0

self.buf.append(msg_byte)
self.rx_msg = PingMessage(msg_data=self.buf)
self.rx_msg = PingMessage.from_buffer(self.buf)

if self.rx_msg.verify_checksum():
self.parsed += 1
Expand Down Expand Up @@ -419,7 +423,7 @@ def parse_byte(self, msg_byte):
0x52,
4,
0,
definitions.COMMON_PROTOCOL_VERSION,
definitions.CommonMessage.PROTOCOL_VERSION,
0,
77,
211,
Expand Down
48 changes: 48 additions & 0 deletions examples/simplePing360Example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env python

#simplePing360Example.py
from brping import Ping360
import time
import argparse

##Parse Command line options
############################

parser = argparse.ArgumentParser(description="Ping python library example (Ping360).",
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--device', help="Ping360 device port. E.g: /dev/ttyUSB0")
parser.add_argument('--baudrate', type=int, default=115200, help="Ping360 device baudrate. E.g: 115200")
parser.add_argument('--udp', help="Ping360 UDP server. E.g: 192.168.2.2:9092")
args = parser.parse_args()
if args.device is None and args.udp is None:
parser.print_help()
exit(1)

# Make a new Ping
device = Ping360()
if args.device is not None:
device.connect_serial(args.device, args.baudrate)
elif args.udp is not None:
(host, port) = args.udp.split(':')
device.connect_udp(host, int(port))

with device:
line = "-" * 40
print(line)
print("Starting Ping360...")
print("Press CTRL+C to exit")
print(line)

input("Press Enter to continue...")

# Read and print
while "user hasn't quit":

# Read and print distance measurements with confidence
while True:
data = myPing.get_distance()
if data:
print("Distance: %s\tConfidence: %s%%" % (data["distance"], data["confidence"]))
else:
print("Failed to get distance data")
time.sleep(0.1)
Loading