mirror of
https://github.com/caperren/school_archives.git
synced 2025-11-09 21:51:15 +00:00
Added 2016-2017 rover code, and a ROB assignment
This commit is contained in:
25
OSU Robotics Club/Mars Rover 2016-2017/common/CMakeLists.txt
Normal file
25
OSU Robotics Club/Mars Rover 2016-2017/common/CMakeLists.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
set(PACKETS_SOURCES
|
||||
"${CMAKE_BINARY_DIR}/common/packets.cpp"
|
||||
)
|
||||
|
||||
set(PACKETS_HEADERS
|
||||
"${CMAKE_BINARY_DIR}/common/packets.h"
|
||||
)
|
||||
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${PACKETS_SOURCES} ${PACKETS_HEADERS}
|
||||
COMMAND python3 basepackets.py
|
||||
ARGS --dest="${CMAKE_BINARY_DIR}/common/"
|
||||
ARGS --src="${CMAKE_SOURCE_DIR}/base/serial/"
|
||||
DEPENDS "${CMAKE_SOURCE_DIR}/common/basepackets.py"
|
||||
DEPENDS "${CMAKE_SOURCE_DIR}/base/serial/packets.h" "${CMAKE_SOURCE_DIR}/base/serial/packets.cpp"
|
||||
WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}/common/"
|
||||
)
|
||||
|
||||
qt5_wrap_cpp(GENERATED_MOC ${PACKETS_HEADERS})
|
||||
|
||||
add_library(packets STATIC ${GENERATED_MOC} ${PACKETS_SOURCES})
|
||||
|
||||
target_link_libraries(packets Qt5::Widgets)
|
||||
5
OSU Robotics Club/Mars Rover 2016-2017/common/README.md
Normal file
5
OSU Robotics Club/Mars Rover 2016-2017/common/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Common Libraries
|
||||
|
||||
This folder contains tools and libraries that can be used by both the electrical and software team. Currently has files for packet IO between the base station and miniboard.
|
||||
|
||||
This was not written by me, but by Nick Ames
|
||||
@@ -0,0 +1,87 @@
|
||||
# Radio Communication Specification
|
||||
## Packet Structure
|
||||
The data radios provide a bidirectional pipe between the rover and the computer (similar to a serial cable). Data is sent LSB first, just as in RS-232. This byte stream is divided into command packets. Each command packet has the following format:
|
||||
|
||||
```
|
||||
<start byte (0x01)>, <length byte>, <2 byte CRC (little-endian)>, <command byte>, [0 or more data bytes]
|
||||
└────────────────────────Packet Header──────────────────────────┘└────────────Packet Body──────────────┘
|
||||
```
|
||||
|
||||
A start byte (value 0x01) begins each packet. (Before receiving a start byte, the receiver should ignore all non-start input bytes.) Following the start byte is a single byte indicating the packet length (as an unsigned integer). The length takes into account all following bytes (the CRC, command bytes, and data byte(s)). After the length byte is the CRC, which is used to validate the contents of the packet after reception. (See below for details on CRC calculation. Packets with an invalid CRC should be silently ignored.) Collectively, the start byte, length byte, and CRC are the packet header.
|
||||
|
||||
Following the packet header is the packet body, which is composed of a command byte (see Packet Types) and zero or more data bytes. Up to 127 bytes of packet data are permitted. The number and meaning of data bytes depends on the command byte, but will be reflected in the length byte in the header.
|
||||
|
||||
Special bytes:
|
||||
Start - 0x01
|
||||
|
||||
## CRC Calculation
|
||||
The CRC is 16-bit with a polynomial of 0x1021 and an initial value 0xFFFF. The CRC is calculated over the packet body (command byte and data byte(s)). The following function will calculate the CRC:
|
||||
|
||||
```c
|
||||
uint16_t calc_crc(uint8_t *body, int body_length){
|
||||
uint16_t remainder = 0xFFFF;
|
||||
for (int i = 0; i < body_length; i++){
|
||||
remainder ^= body[i] << 8;
|
||||
for (int bit = 8; bit > 0; bit--){
|
||||
if (remainder & 0x8000){
|
||||
remainder = (remainder << 1) ^ 0x1021;
|
||||
} else {
|
||||
remainder = (remainder << 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return remainder;
|
||||
}
|
||||
```
|
||||
|
||||
After the CRC has been calculated, it should be checked for agreement with the CRC in the packet header. If they disagree, the packet should be silently discarded.
|
||||
|
||||
## Packet Types
|
||||
Commands are sent from the computer to the rover, and result in a reply from the rover to the computer. Command execution and the reply occur after the entire length of the packet is received and verified against the CRC. Conceptually, commands can be thought of as reading or writing values in the rover's memory. The MSB (bit 7, 0x80) of the command byte indicates whether that command is reading or writing (0 = writing, 1 = reading). When reading, the command should contain no data.
|
||||
For each command, the rover sends a reply containing the command (in the same read/write form as it received it) and any additional data. For write commands, no data is sent in the reply. For read commands, the requested data is sent in the reply. If the rover receives a command it does not recognize, it sends a reply with a command byte of 0x00. If the computer attempts to write a read-only register, the rover acts as if the write succeeded.
|
||||
|
||||
Multi-byte values are transmitted little-endian. Two’s complement is used for signed values.
|
||||
In the following table, argument specifications begin with a code (i8 = 8-bit int, u16 = 16-bit unsigned int, etc) indicating the size of each argument. Any variable length arguments are specified using a star (\*). The leftmost argument is the first in the packet data.
|
||||
Camera video data is transmitted through a separate interface. Camera commands and camera selection are sent through this command protocol.
|
||||
|
||||
Note: when adding commands to the table, each argument's name must be unique among all commands. Since they are used in the rover firmware,
|
||||
don't change the name of existing command arguments.
|
||||
|
||||
## Packet Type Specification
|
||||
| Name | RW | Command Code | Arguments | Default values | Notes |
|
||||
| ---- | --- | ------------ | --------- | -------------- | ----- |
|
||||
| Command not Recognized | - | 0x00 | u8 wrong_command | - | Sent as a reply to unknown commands. |
|
||||
| Pause | RW | 0x05 | u8 pause_state | 1 | 0 = pause (no rover motion) 1 = unpause |
|
||||
| Battery Voltage | R | 0x06 | u16 battery_voltage | - | Battery voltage in mV |
|
||||
| Drive Motor Power | RW | 0x10 | i8 l_f_drive, i8 l_m_drive, i8 l_b_drive, i8 r_f_drive, i8 r_m_drive, i8 r_b_drive | 0,0,0,0,0,0 | -127 = full reverse 128 = full forward, r = right, l = left, f = front, m = middle, b = back |
|
||||
| Swerve Drive State | RW | 0x11 | u8 swerve_state | 0 | 0x00 = Off (no motion), 0x01 = Straight, 0x02 = Turn |
|
||||
| Arm Motors | RW | 0x12 | i8 arm_motor_1, i8 arm_motor_2, i8 arm_motor_3, i8 arm_motor_4, i8 arm_motor_5 | 0,0,0,0,0 | -127 = full reverse 128 = full forward TODO: Define motor->joint mapping
|
||||
| Servo | W | 0x14 | u8 ax12_addr, u16 ax12_angle | 0,512 | Set the target angle of an AX12 servo. |
|
||||
| S Bus Values 1 | R | 0x15 | u16 sbus_1, u16 sbus_2, u16 sbus_3, u16 sbus_4, u16 sbus_5, u16 sbus_6, u16 sbus_7, u16 sbus_8 | - | S-Bus channel values. |
|
||||
| S Bus Values 2 | R | 0x16 | u16 sbus_9, u16 sbus_10, u16 sbus_11, u16 sbus_12, u16 sbus_13, u16 sbus_14, u16 sbus_15, u16 sbus_16, u8 sbus_active | - | S-Bus channel values plus a status bit indicating if the failsafe has been set. |
|
||||
| Select Camera | RW | 0x20 | u8 selected_camera | 5 | 1-6; select camera feed to send to the base station and to send commands to. Note: camera output will be disabled unless the callsign has been set. TODO: define which camera corresponds to which number. |
|
||||
| Callsign | RW | 0x21 | u8 callsign_data_length, * callsign_data| - | ASCII string of callsign (use numerals and capital letters only) |
|
||||
| Camera Command | W | 0x22 | u8 camera_data_length, * camera_data | - | Custom camera commands defined in camera manual. camera_data_length defines the number of data bytes in the command (0-255). camera_data is the command to be sent to the camera.|
|
||||
| GPS Position | R | 0x23 | u8 gps_pos_valid, i64 latitude, i64 longitude, i32 altitude | 0,0,0,0 | GPS Position. Good when valid != 0. Sign meaning: +=north/east. Latitude and longitude are in 10e-6 minute units. Altitude is in meters. |
|
||||
| GPS Track | R | 0x24 | u8 gps_track_valid, i16 gps_heading, u16 gps_speed | 0,0,0 | GPS Heading, in hundredths of a degree. Speed in meters/hour. |
|
||||
| Magnetometer | R | 0x26 | i16 mag_x, i16 mag_y, i16 mag_z | 0,0,0 | External magnetometer reading. TODO: define units and axis directions. |
|
||||
| Accelerometer | R | 0x27 | i16 accel_x, i16 accel_y, i16 accel_z | 0,0,0 | IMU accelerometer reading. TODO: define units and axis directions. |
|
||||
| Gyroscope | R | 0x28 | i16 gyro_x, i16 gyro_y, i16 gyro_z | 0,0,0 | IMU gyroscope reading. TODO: define units and axis directions. |
|
||||
| Compass Heading | R | 0x29 | u8 compass_heading_valid, i16 compass_heading | 0,0 | Magnetic compass heading. TODO: define units and axis directions. |
|
||||
| Pan Tilt Speed | RW | 0x2B | i8 pan_speed, i8 tilt_speed | 0,0 | Pan/tilt speeds. Both range from -128 to 127. The total angle will be limited to safe values.|
|
||||
| AX12 Arm Mode | RW | 0x2C | u8 arm_mode | 0 | 0 = arm AX12s not in use, 1 = arm ax12s in use |
|
||||
| End Effector Speed | RW | 0x2D | i16 ee_speed | 0 | Speed of end effector pitch control. Range -1023 to 1023. |
|
||||
| Grabber | RW | 0x2E | i16 grabber_speed, i16 grabber_rotation_speed | 0,0 | Speed of grabber motors. Range -1023 to 1023. |
|
||||
| Container Sealer | RW | 0x2F | u16 cflex1_speed, u16 cflex2_speed, i16 cseal_speed | 0,0,0 | Angles and speed of container motors. |
|
||||
| GPIO Read State | R | 0x32 | u8 gpio_state | 0 | GPIO pin directions. 1 = high, 0 = low. Mapping: MSB X X 5 4 3 2 1 X LSB. |
|
||||
| Sample Camera Action | RW | 0x35 | u8 cam_action | 0 | Sample cam actions. 0 = none, 4 = shutter, 3 = focus, 1 = zoom in, 2 = zoom out |
|
||||
| Navigation Camera Action | RW | 0x36 | u8 nav_action | 0 | Naviagation cam actions. 0 = none, 1 = zoom in, 2 = zoom out |
|
||||
| Soil Sensor Send | W | 0x40 | u8 soil_send_data_length, * soil_send_data| - | Data string to send to the soil sensor. |
|
||||
| Soil Sensor Recv | RW | 0x41 | u8 soil_recv_data_length, * soil_recv_data| - | Reply string received from the soil sensor. |
|
||||
| Soil Measure | RW | 0x42 | u8 soil_measure | 0 | 0 = do nothing, 1 = take measurement, 2 = measurement complete |
|
||||
| Soil Measurements | R | 0x43 | i32 moisture, i32 temperature, i32 salinity | 0, 0, 0 | Divide each by 1000 to get the floating-point value. |
|
||||
| Joystick | RW | 0x50 | i8 fr_joylh, i8 fr_joylv, i8 fr_joyrh, i8 fr_joyrv, i8 fr_potl, i8 fr_potr, i8 fr_sidel, i8 fr_sider, u8 fr_buttons, i8 xbox_joylh, i8 xbox_joylv, i8 xbox_joyrh, i8 xbox_joyrv, i8 xbox_triggerl, i8 xbox_triggerr, u8 xbox_buttons_high, u8 xbox_buttons_low | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 | Frsky buttons are packed (MSB) H ..A (LSB). MSB of xbox_buttons_high is enable; must be 1! XBOX low byte -> (MSB)(RSC)(LSC)(RB)(LB)(Y)(X)(B)(A)(LSB) XBOX high byte (MSB)(1)(DPD)(DPU)(DPR)(DPL)(HOME)(STRT)(SEL)(LSB)|
|
||||
| Autonomous Enable | RW | 0x60 | u8 auton_en | 0 | If 1, do autonomous traversal. |
|
||||
| Autonomous Waypoint 1 | RW | 0x61 | i64 auton_way1_lat, i64 auton_way1_lon, u16 auton_way1_speed | 0,0,0 | Autonomous target waypoint. Speed in mm/s. |
|
||||
| Autonomous Waypoint 2 | RW | 0x63 | i64 auton_way2_lat, i64 auton_way2_lon, u16 auton_way2_speed | 0,0,0 | Autonomous next waypoint. Speed in mm/s. |
|
||||
| Time ms | R | 0x64 | u32 time_ms | 0 | Time since rover was turned on, in ms. |
|
||||
618
OSU Robotics Club/Mars Rover 2016-2017/common/basepackets.py
Normal file
618
OSU Robotics Club/Mars Rover 2016-2017/common/basepackets.py
Normal file
@@ -0,0 +1,618 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
|
||||
"""This packet generates C++ files for handling packets on the
|
||||
side of the base station."""
|
||||
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
|
||||
class BasePackets(object):
|
||||
"""
|
||||
Autogenerates C++ code to encode and decode packets sent between the base
|
||||
station and the miniboard.
|
||||
|
||||
This class takes in template C++ source and header files and expands them
|
||||
to include Qt signals emitted when a packet of a specific type is parsed
|
||||
successfully, as well as slots that allow for sending packets to the
|
||||
miniboard. The input files should contain comments with an "@" followed by a
|
||||
command or attributes.
|
||||
|
||||
Atttributes are variables defined in the header files that will be used in
|
||||
constructing autogenerated functions. They are specified with the
|
||||
attribute name followed by a colon and the name of the variable in the
|
||||
source file. For example, writing "@datastream: m_datastream" will specify
|
||||
that the QDataStream class to read and write data from is called
|
||||
m_datastream. Currently, available attributes are "datastream", designating
|
||||
the QDataStream to use, the "types_enum", the enum that stores the codes
|
||||
for the packet types, and the "crc", the name of the CRC function.
|
||||
|
||||
Additionally, functions denoted by an "@" in the source provide the location
|
||||
for expanding the file during the autogeneration process. These functions
|
||||
match the names in this class. Available functions are:
|
||||
"packet_types_header", defining where to enumerate the packet codes,
|
||||
"recieved_signals_header", header functions for the recieved signals,
|
||||
"write_slots_header", header functions for slots that send packets with
|
||||
for writing,
|
||||
"read_slots_header", header functions for slots that read data from the
|
||||
miniboard,
|
||||
"parse_packets", the source function that parses a packet, it takes in
|
||||
a parameter specifying the size of the packet, such as
|
||||
"@parse_packets(size)",
|
||||
"write_slots_source", write the source code for write slots, and
|
||||
"read_slots_source", source code for read slots.
|
||||
"""
|
||||
def __init__(self, specification=None, files={}):
|
||||
"""
|
||||
Creates an instance of the BasePackets class.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
specification : string
|
||||
The path to the packet specification file.
|
||||
files : dictionary
|
||||
Dictionary of the input and output files. Input files contain
|
||||
special commands that the parser will recognize. The autogenerated
|
||||
files will be written to the output files. The format for the
|
||||
dictionary is {"input_file": "output_file"}.
|
||||
"""
|
||||
# TODO: normalized naming to template file (low priority)
|
||||
# files {"input_file": "output_file"}
|
||||
self._specification = specification
|
||||
self.files = files
|
||||
|
||||
# packets generated by the extract_table function
|
||||
self._packets = None
|
||||
# parameters extracted from header files, such as "datastream", etc.
|
||||
self._params = {"start_byte": 0x01}
|
||||
self._tabsize = 4
|
||||
# requires removing all whitespace
|
||||
# TODO: make not use directly
|
||||
# regular expressions for various parsing and extraction
|
||||
self._param_extractor = re.compile(r"(?<=:)[a-zA-Z0-9_]+")
|
||||
# TODO: try (?<=^class) (low priority)
|
||||
self._class_extractor = re.compile(r"(?<=class )[a-zA-Z]+")
|
||||
self._function_arg_extractor = re.compile(r"\(.+\)")
|
||||
self._arg_size_extractor = re.compile(r"(\d|\*)+$")
|
||||
|
||||
def packets():
|
||||
doc = """A list of packets and their properties. The format is
|
||||
specified in the extrac_table function."""
|
||||
def fget(self):
|
||||
return self._packets
|
||||
return locals()
|
||||
packets = property(**packets())
|
||||
|
||||
def specification():
|
||||
doc = """The path to the packet specification file."""
|
||||
def fget(self):
|
||||
return self._specification
|
||||
def fset(self, value):
|
||||
self._specification = value
|
||||
return locals()
|
||||
specification = property(**specification())
|
||||
|
||||
def files():
|
||||
doc = """Dictionary of files used in the parsing and autogeneration
|
||||
process. Files are stored in an ordered dictionary with source
|
||||
(.cpp) files at the end."""
|
||||
def fget(self):
|
||||
return self._files
|
||||
def fset(self, value):
|
||||
self._files = OrderedDict(sorted(value.items(),
|
||||
key=lambda x: x[0].endswith(".cpp")))
|
||||
return locals()
|
||||
files = property(**files())
|
||||
|
||||
def write_files(self):
|
||||
"""Parses the input files and outputs the autogenerated files. This is
|
||||
the function to call after the necessary files have been specified."""
|
||||
for t, s in self._files.items():
|
||||
self._write_file(str(t), str(s))
|
||||
|
||||
def _write_file(self, template, output):
|
||||
"""Parses an individual file. Extracts packets from the specification if
|
||||
not done so already, then looks for matching functions in the template
|
||||
and calls the respective function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
template : string
|
||||
The path to the template used for autogenerating the output.
|
||||
output : string
|
||||
The path to the output file.
|
||||
"""
|
||||
if self._specification is None:
|
||||
raise ValueError("The specification file has not been specified.")
|
||||
if self._packets is None:
|
||||
self.extract_table()
|
||||
|
||||
with open(template) as f:
|
||||
file_template = f.read().splitlines()
|
||||
|
||||
with open(output, "w+") as f:
|
||||
for line in file_template:
|
||||
f.write(line + "\n")
|
||||
if "class" in line:
|
||||
if "enum" in line:
|
||||
continue
|
||||
self._params.update({"class":
|
||||
re.search(self._class_extractor, line).group(0)})
|
||||
if "@" not in line:
|
||||
continue
|
||||
elif "@datastream" in line:
|
||||
self._params.update({"datastream":
|
||||
self._extract_param(line)})
|
||||
elif "@types_enum" in line:
|
||||
self._params.update({"types_enum":
|
||||
self._extract_param(line)})
|
||||
elif "@crc" in line:
|
||||
self._params.update({"crc":
|
||||
self._extract_param(line)})
|
||||
elif "@packet_types_header" in line:
|
||||
f.write(self.packet_types_header(line=line))
|
||||
elif "@recieved_signals_header" in line:
|
||||
f.write(self.packet_signals_header(line=line))
|
||||
elif "@write_slots_header" in line:
|
||||
f.write(self.write_slots_header(line=line))
|
||||
elif "@read_slots_header" in line:
|
||||
f.write(self.read_slots_header(line=line))
|
||||
elif "@parse_packets" in line:
|
||||
f.write(self.parse_packets(line=line))
|
||||
elif "@write_slots_source" in line:
|
||||
f.write(self.write_slots_source(line=line))
|
||||
elif "@read_slots_source" in line:
|
||||
f.write(self.read_slots_source(line=line))
|
||||
|
||||
def extract_table(self):
|
||||
"""Extract the command table from the text of the specification file.
|
||||
Creates a list of dictionaries with the following members:
|
||||
"name" : str, name of command
|
||||
"rw" : str, contains r is command can be read, w if command
|
||||
can be written, - if neither
|
||||
"code" : int, command code (0-127)
|
||||
"argument" : list, list of 0 or more tuples, each with the
|
||||
following structure:
|
||||
tuple(format code:str, command format code, u8, i16,
|
||||
etc, or * to indicate a variable number of bytes.
|
||||
name:str))
|
||||
"default" : list of default values of arguments, as strings
|
||||
"notes" : str, notes on command."""
|
||||
with open(self._specification, "r") as f:
|
||||
file_str = f.read()
|
||||
|
||||
s = file_str.find("\n| Name | RW | Command Code")
|
||||
s += 1 # Skip over first newline
|
||||
e = file_str[s:].find("\n\n")
|
||||
if e < s:
|
||||
table_str = file_str[s:]
|
||||
else:
|
||||
table_str = file_str[s:s + e]
|
||||
self._packets = []
|
||||
for l in table_str.split("\n"):
|
||||
if len(l) == 0: continue
|
||||
if l[0] == "|":
|
||||
l = l[1:]
|
||||
col = [c.strip() for c in l.split("|")]
|
||||
if col[0][0:3] == "---": continue
|
||||
if col[0] == "Name": continue
|
||||
name = col[0]
|
||||
rw = col[1].lower()
|
||||
code = int(col[2], 0)
|
||||
notes = col[5]
|
||||
argument = []
|
||||
for a in [c.strip() for c in col[3].split(",")]:
|
||||
a = a.split(" ")
|
||||
if len(a) == 2:
|
||||
argument.append((a[0], a[1]))
|
||||
default = []
|
||||
if len(argument) > 0 and col[4].strip() != "-":
|
||||
for d in [c.strip() for c in col[4].split(",")]:
|
||||
default.append(d)
|
||||
if len(default) != len(argument) and len(default) != 0:
|
||||
raise RuntimeError(
|
||||
"Default list length mismatch on command %s" % name
|
||||
)
|
||||
self._packets.append({
|
||||
"name": name,
|
||||
"rw": rw,
|
||||
"code": code,
|
||||
"argument": argument,
|
||||
"default": default,
|
||||
"notes": notes
|
||||
})
|
||||
# removing redundant packet arguments
|
||||
for packet in self._packets:
|
||||
for i, arg in enumerate(packet["argument"]):
|
||||
if "length" in arg[1]:
|
||||
packet["argument"].pop(i)
|
||||
|
||||
|
||||
def packet_types_header(self, line=None):
|
||||
"""
|
||||
Generates header function for the packet types.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : string, optional
|
||||
The string line that contains the command. Used for controlling
|
||||
the amount of trailing whitespace.
|
||||
"""
|
||||
string = ""
|
||||
ws = self._whitespace(line)
|
||||
for packet in self._packets:
|
||||
uppercase_name = packet["name"].upper().replace(" ", "_")
|
||||
string += ws + ("%s = %i,\n" % (uppercase_name, packet["code"]))
|
||||
return string
|
||||
|
||||
def packet_signals_header(self, line=None):
|
||||
"""
|
||||
Generates header function for the packet signals.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
line : string, optional
|
||||
The string line that contains the command. Used for controlling
|
||||
the amount of trailing whitespace.
|
||||
"""
|
||||
string = ""
|
||||
ws = self._whitespace(line)
|
||||
for packet in self._packets:
|
||||
if "r" in packet["rw"]:
|
||||
|
||||
string += (ws + "void " + self._signal_name(packet) + "(" +
|
||||
self._argument_proto(packet) + ");\n")
|
||||
return string
|
||||
|
||||
def write_slots_header(self, line=None):
|
||||
string = ""
|
||||
ws = self._whitespace(line)
|
||||
for packet in self._packets:
|
||||
if "w" in packet["rw"]:
|
||||
string += (ws + "void write" + self._camelcase(packet["name"]) +
|
||||
"(" + self._argument_proto(packet) + ");\n")
|
||||
return string
|
||||
|
||||
def read_slots_header(self, line=None):
|
||||
string = ""
|
||||
ws = self._whitespace(line)
|
||||
for packet in self._packets:
|
||||
if "r" in packet["rw"]:
|
||||
string += (ws + "void read" + self._camelcase(packet["name"]) +
|
||||
"();\n")
|
||||
return string
|
||||
|
||||
def parse_packets(self, line=None):
|
||||
# note that parse_packets assumes that the datastream head is at the
|
||||
# beginning of the crc
|
||||
ws = self._whitespace(line)
|
||||
string = ""
|
||||
# bitshift in the crc, and the packet type
|
||||
string += ws + "quint16 _read_crc;\n"
|
||||
string += ws + ("%s >> _read_crc;\n" % self._params["datastream"])
|
||||
string += ws + "quint8 _packetType;\n"
|
||||
string += ws + ("%s >> _packetType;\n" % self._params["datastream"])
|
||||
|
||||
# index to specify whether to use "if" or "else if"
|
||||
i = 0
|
||||
for packet in self._packets:
|
||||
uppercase_name = packet["name"].upper().replace(" ", "_")
|
||||
if "r" not in packet["rw"]:
|
||||
continue
|
||||
|
||||
if i is 0:
|
||||
i += 1
|
||||
conditional = "if"
|
||||
else:
|
||||
conditional = "else if"
|
||||
|
||||
string += (ws + "%s(_packetType == (static_cast<quint8>"
|
||||
"(%s::%s) | 0x80)) {\n" % (
|
||||
conditional,
|
||||
self._params["types_enum"],
|
||||
uppercase_name)
|
||||
)
|
||||
packet_size = self._packet_size(packet)
|
||||
# TODO: what if the packet size is variable? This requires adding
|
||||
# in a size argument to the parse_packets function, creating a data
|
||||
# type of that size, and bitshifting in the data.
|
||||
# Note: this will probably require using readRawBytes or readBytes
|
||||
# function.
|
||||
if packet_size is not None:
|
||||
for arg in packet["argument"]:
|
||||
# for every argument in the packet, create a new variable
|
||||
# and bitshift it in from the stream
|
||||
arg_type = self._expand_argument(arg[0])
|
||||
string += ws + "\t%s %s;\n" % (arg_type, arg[1])
|
||||
string += ws + "\t%s >> %s;\n" % (
|
||||
self._params["datastream"], arg[1]
|
||||
)
|
||||
string += ws + '\tqDebug() << "argument: " << "%s" << "; value: " << %s;\n' % (
|
||||
arg[1], arg[1]
|
||||
)
|
||||
|
||||
string += self._crc_calculation(packet, ws=ws, write=True)
|
||||
|
||||
# TODO: checking against crc (high priority)
|
||||
# then emit
|
||||
string += ws + "\tif(_crc == _read_crc) {\n"
|
||||
string += ws + '\t\tqDebug() << "CRC check successful.";\n'
|
||||
string += ws + "\t\temit " + self._signal_name(packet) + "("
|
||||
string += "".join(map(lambda x: x[1] + ", ",
|
||||
packet["argument"])).strip(" ,")
|
||||
string += ");\n"
|
||||
string += ws + "\t}\n"
|
||||
|
||||
else:
|
||||
string += ws + "\tchar _data[size];\n" # TODO: remove hard coding
|
||||
string += ws + "\t%s.readRawData(_data, size);\n" % (
|
||||
self._params["datastream"],
|
||||
)
|
||||
string += ws + "\tQByteArray _byte_array = QByteArray(_data);\n"
|
||||
|
||||
|
||||
string += ws + "\tquint16 _crc = 0xFFFF;\n"
|
||||
|
||||
string += ws + "\t_crc = %s(&_packetType, sizeof(quint8), _crc);\n" % (
|
||||
self._params["crc"],
|
||||
)
|
||||
string += ws + "\t_crc = %s(&_byte_array, size, _crc);\n" % (
|
||||
self._params["crc"]
|
||||
)
|
||||
string += ws + '\tqDebug() << "argument: %s; value: "' % (
|
||||
packet["argument"][-1][1]
|
||||
)
|
||||
string += " << _byte_array;\n"
|
||||
string += ws + "\tif(_crc == _read_crc) {\n"
|
||||
string += ws + "\t\temit %s(_byte_array);\n" % (
|
||||
self._signal_name(packet)
|
||||
)
|
||||
string += ws + "\t}\n"
|
||||
# TODO
|
||||
string += ws + "}\n"
|
||||
|
||||
for packet in self._packets:
|
||||
uppercase_name = packet["name"].upper().replace(" ", "_")
|
||||
if "w" not in packet["rw"]:
|
||||
continue
|
||||
string += (ws + "else if(_packetType == static_cast<quint8>"
|
||||
"(%s::%s)) {\n") % (
|
||||
self._params["types_enum"],
|
||||
uppercase_name
|
||||
)
|
||||
string += ws + "\tquint16 _crc = 0xFFFF;\n"
|
||||
string += ws + "\t_crc = %s(&_packetType, sizeof(quint8), _crc);\n" % (
|
||||
self._params["crc"],
|
||||
)
|
||||
string += ws + "\tif(_crc == _read_crc) {\n"
|
||||
string += ws + '\t\tqDebug() << "Confirmed packet reception.";\n'
|
||||
string += ws + "\t}\n"
|
||||
# TODO: don't do anything yet, but to be added
|
||||
# TODO: like a qDebug statement
|
||||
string += ws + "}\n"
|
||||
|
||||
|
||||
return string.expandtabs(self._tabsize)
|
||||
|
||||
|
||||
def write_slots_source(self, line=None):
|
||||
string = ""
|
||||
for packet in self._packets:
|
||||
if "w" not in packet["rw"]:
|
||||
continue
|
||||
string += ("void %s::write%s(%s)\n{\n" % (
|
||||
self._params["class"],
|
||||
self._camelcase(packet["name"]),
|
||||
self._argument_proto(packet),
|
||||
))
|
||||
string += '\tqDebug() << "Beginning packet write.";\n'
|
||||
string += "\tquint8 _packetType = static_cast<quint8>(%s::%s);\n" % (
|
||||
self._params["types_enum"],
|
||||
self._uppercase_name(packet),
|
||||
)
|
||||
|
||||
string += self._crc_calculation(packet)
|
||||
|
||||
string += "\t%s << (quint8)%s;\n" % (
|
||||
self._params["datastream"],
|
||||
self._params["start_byte"],
|
||||
)
|
||||
if self._packet_size(packet) is not None:
|
||||
string += "\t%s << (quint8)%s;\n" % (
|
||||
self._params["datastream"],
|
||||
self._packet_size(packet)
|
||||
)
|
||||
else:
|
||||
string += "\t%s << static_cast<quint8>(%s.size() + 3);\n" % (
|
||||
self._params["datastream"],
|
||||
packet["argument"][-1][1],
|
||||
)
|
||||
string += "\t%s << _crc;\n" % self._params["datastream"]
|
||||
string += "\t%s << _packetType;\n" % (
|
||||
self._params["datastream"],
|
||||
)
|
||||
if self._packet_size(packet) is not None:
|
||||
for arg in packet["argument"]:
|
||||
string += '\tqDebug("writing: %s; value: %i",' + '"%s", %s);\n' % (
|
||||
arg[1], arg[1]
|
||||
)
|
||||
string += "\t%s << %s;\n" % (
|
||||
self._params["datastream"], arg[1]
|
||||
)
|
||||
string += '\tqDebug() << "argument:" << "%s" << "; value:" << %s;\n' % (
|
||||
arg[1], arg[1]
|
||||
)
|
||||
else:
|
||||
string += "\t%s.writeRawData(%s.constData(), %s.size());\n" % (
|
||||
self._params["datastream"],
|
||||
packet["argument"][-1][1],
|
||||
packet["argument"][-1][1],
|
||||
)
|
||||
|
||||
string += "}\n\n"
|
||||
return string.expandtabs(self._tabsize)
|
||||
|
||||
def _crc_calculation(self, packet, ws="", write=True, packet_type="_packetType"):
|
||||
string = ""
|
||||
string += ws + "\tquint16 _crc = 0xFFFF;\n"
|
||||
string += ws + "\t_crc = %s(&%s, sizeof(quint8), _crc);\n" % (
|
||||
self._params["crc"],
|
||||
packet_type,
|
||||
)
|
||||
if not write:
|
||||
return string
|
||||
|
||||
if self._packet_size(packet) is not None:
|
||||
for arg in packet["argument"]:
|
||||
string += ws + "\t_crc = %s(&%s, sizeof(%s), _crc);\n" % (
|
||||
self._params["crc"],
|
||||
arg[1],
|
||||
arg[1], # TODO: fix this, unworking for variable size packets
|
||||
)
|
||||
else:
|
||||
string += ws + "\t_crc = %s(&%s, %s.size(), _crc);\n" % (
|
||||
self._params["crc"],
|
||||
packet["argument"][-1][1],
|
||||
packet["argument"][-1][1],
|
||||
)
|
||||
return string
|
||||
|
||||
def read_slots_source(self, line=None):
|
||||
string = ""
|
||||
for packet in self._packets:
|
||||
if "r" not in packet["rw"]:
|
||||
continue
|
||||
string += "void %s::read%s()\n{\n" % (
|
||||
self._params["class"],
|
||||
self._camelcase(packet["name"]),
|
||||
)
|
||||
string += '\tqDebug() << "Beginning packet write.";\n'
|
||||
string += ("\tquint8 _packetType = static_cast<quint8>(%s::%s) "
|
||||
"| 0x80;\n") % (
|
||||
self._params["types_enum"],
|
||||
self._uppercase_name(packet),
|
||||
)
|
||||
|
||||
string += self._crc_calculation(packet, write=False)
|
||||
string += "\t%s << (quint8)%s;\n" % (
|
||||
self._params["datastream"],
|
||||
self._params["start_byte"],
|
||||
)
|
||||
|
||||
string += "\t%s << (quint8)3;\n" % self._params["datastream"]
|
||||
string += "\t%s << _crc;\n" % self._params["datastream"]
|
||||
string += "\t%s << _packetType;\n" % (
|
||||
self._params["datastream"],
|
||||
)
|
||||
|
||||
string += "}\n\n"
|
||||
return string.expandtabs(self._tabsize)
|
||||
|
||||
|
||||
def _parse_function_args(self, str):
|
||||
arguments = re.search(self._function_arg_extractor, str).group(0)
|
||||
if arguments is None:
|
||||
return None
|
||||
arguments = arguments.strip("() ")
|
||||
arguments = list(map(lambda x: x.strip(" ,"), arguments))
|
||||
return arguments
|
||||
|
||||
def _signal_name(self, packet):
|
||||
return (self._camelcase(packet["name"], capitalize_first_word=False) +
|
||||
"Received")
|
||||
|
||||
def _expand_argument(self, arg):
|
||||
if arg == "*":
|
||||
arg_type = "QByteArray"
|
||||
elif "u" in arg:
|
||||
arg_type = "q" + arg.replace("u", "uint")
|
||||
elif "i" in arg:
|
||||
arg_type = "q" + arg.replace("i", "int")
|
||||
else:
|
||||
raise ValueError("Argument type %s id not recognized" % arg)
|
||||
return arg_type
|
||||
|
||||
def _argument_proto(self, packet):
|
||||
argument_str = ""
|
||||
for arg in packet["argument"]:
|
||||
arg_type = self._expand_argument(arg[0])
|
||||
argument_str += arg_type + " " + arg[1] + ", "
|
||||
argument_str = argument_str.strip(" ,")
|
||||
return argument_str
|
||||
|
||||
def _camelcase(self, string, capitalize_first_word=True):
|
||||
"""Takes a string of words and returns a camelcase version.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
string : string
|
||||
This is a string of space-separated words to be used in constructing
|
||||
a camelcase string.
|
||||
capitalize_first_word : bool, optional
|
||||
Specifies whether to capitalize the first word.
|
||||
|
||||
Returns
|
||||
-------
|
||||
words : string
|
||||
A camelcase version of the input string. For example, "this is a
|
||||
string" would return "ThisIsAString" or "thisIsAString".
|
||||
"""
|
||||
words = string.split(" ")
|
||||
words = map(lambda x: x.capitalize(), words)
|
||||
words = "".join(words)
|
||||
if not capitalize_first_word:
|
||||
words = words[0].lower() + words[1:]
|
||||
return words
|
||||
|
||||
def _whitespace(self, reference):
|
||||
if reference is None:
|
||||
return ""
|
||||
num_spaces = (len(reference) - len(reference.lstrip(" ")))
|
||||
return " " * num_spaces
|
||||
|
||||
def _packet_size(self, packet):
|
||||
# in bytes
|
||||
# add two bytes for CRC, and one byte for packet type
|
||||
size = 3
|
||||
for arg in packet["argument"]:
|
||||
arg_size = re.search(self._arg_size_extractor, arg[0]).group(0)
|
||||
if arg_size is None:
|
||||
raise ValueError("Cannot determine size of packet.")
|
||||
elif arg_size == "*":
|
||||
return None
|
||||
arg_size = int(arg_size)
|
||||
if (arg_size % 8 is not 0):
|
||||
|
||||
raise ValueError("Argument sizes must be multiples of 8.")
|
||||
size += (arg_size // 8)
|
||||
|
||||
return size
|
||||
|
||||
def _uppercase_name(self, packet):
|
||||
return packet["name"].upper().replace(" ", "_")
|
||||
|
||||
def _extract_param(self, string):
|
||||
s = string.replace(" ", "")
|
||||
return re.search(self._param_extractor, s).group(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
import os.path
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--src", type=str, default=".")
|
||||
parser.add_argument("--dest", type=str, default=".")
|
||||
parser.add_argument("--name", type=str, default="packets")
|
||||
parser.add_argument("--spec", type=str, default="SPECIFICATION.md")
|
||||
args = parser.parse_args()
|
||||
header_template = os.path.join(args.src, args.name) + ".h"
|
||||
header_output = os.path.join(args.dest, args.name) + ".h"
|
||||
source_template = os.path.join(args.src, args.name) + ".cpp"
|
||||
source_output = os.path.join(args.dest, args.name) + ".cpp"
|
||||
specification = args.spec
|
||||
b = BasePackets(
|
||||
specification=specification,
|
||||
files = {header_template: header_output, source_template: source_output}
|
||||
)
|
||||
b.write_files()
|
||||
190
OSU Robotics Club/Mars Rover 2016-2017/common/codegen.py
Normal file
190
OSU Robotics Club/Mars Rover 2016-2017/common/codegen.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
#Generates communications code for the miniboard firmware, based on
|
||||
#a command protocol specification document (see docparse.py) supplied
|
||||
#as the first command-line argument.
|
||||
#
|
||||
#This code generation system has two goals: to allow commands to be easily added
|
||||
#to the spec prior to their implementation, and to ensure that all commands
|
||||
#function consistently. To accomplish this, all command-specific communication
|
||||
#code is generated automatically from the specification document.
|
||||
#For this to be possible, the code that actually carries out each command
|
||||
#must be placed elsewhere.
|
||||
#
|
||||
#Conceptually, commands to the rover read and write registers in its memory.
|
||||
#This model makes separating communication and action easy. The automatically-generated
|
||||
#communication code reads and writes fields in a large global struct (also automatically
|
||||
#generated), with each field corresponding to a command argument.
|
||||
#
|
||||
#The output of this program is a source file and a header file containing the auto-generated
|
||||
#communication code.
|
||||
#The global structure is called DataReal. Due to issues with the volatile qualifier
|
||||
#(the main code needs it, but the ISR doesn't), the main code accesses DataReal through
|
||||
#a pointer called Data.
|
||||
#The parse_packet(uint8_t *buf, uint16_t count) function initiates packet
|
||||
#parsing and dispatches the appropriate reply. buf must contain the command
|
||||
#byte as the first byte in the buffer. count must be the number of bytes in
|
||||
#the packet, not including the start or end bytes, and with escape bytes removed.
|
||||
#
|
||||
#The host program must provide a send_packet(uint8_t *data, uint16_t count) function
|
||||
#which adds start/end bytes and escapes special characters, then sends the packet
|
||||
#over the radio.
|
||||
#The host program must also provide a memcpy() function.
|
||||
#
|
||||
#Additionally, there is an asynchronous command element beyond the simple read/write memory
|
||||
#model. When a command includes a variable-length argument, a <command name>_trigger(uint8_t *data, uint16_t *len)
|
||||
#function will be called when the command is received. This function must be implemented by
|
||||
#the host code. The purpose of this mechanism is to notify the action code when the data
|
||||
#changes. The trigger function must be capable of being run in an interrupt.
|
||||
import sys
|
||||
from docparse import *
|
||||
|
||||
def get_all_args(cmd_list):
|
||||
"""Return a list of tuples (format code, argument name, note comment)."""
|
||||
l = list()
|
||||
for c in cmd_list:
|
||||
for a in c["argument"]:
|
||||
l += [(a[0], a[1], c["notes"])]
|
||||
return l
|
||||
|
||||
def gen_struct_def(cmd_list):
|
||||
s = "struct comm_data_t {\n"
|
||||
for c in get_all_args(cmd_list):
|
||||
dt = format_code_to_cstdint(c[0])
|
||||
if "*" in dt:
|
||||
#Arrays
|
||||
s += "\t" + dt[0:-2] + " " + c[1] + "[%d];"%(2 ** int(dt[4:5])) + " /* " + c[2] + " */\n"
|
||||
else:
|
||||
s += "\t" + dt + " " + c[1] + ";" " /* " + c[2] + " */\n"
|
||||
s += "};\n\n"
|
||||
return s
|
||||
|
||||
def gen_build_str_def():
|
||||
"""Return a declaration for a progmem string containing information about the build."""
|
||||
return ""
|
||||
|
||||
def gen_build_str_dec():
|
||||
"""Return a definition of a PROGMEM string containing information about the build."""
|
||||
#Get name of person building firmware
|
||||
#git config --get-all user.name
|
||||
#Get repo revision
|
||||
#git log | head -1 | cut -d " " -f 2
|
||||
#Get branch
|
||||
#git branch | grep "\*" | cut -d " " -f 2
|
||||
#Get modified status
|
||||
#Date, time, gcc version (__VERSION__)
|
||||
s = "Miniboard Firmware rev "
|
||||
return ""
|
||||
|
||||
|
||||
def gen_header(cmd_list):
|
||||
"""Return a string containing the C header for the communication module."""
|
||||
s = "/* Warning: This file is automatically generated. Do not modify. */\n"
|
||||
s += "#ifndef COMMGEN_H\n"
|
||||
s += "#define COMMGEN_H\n\n"
|
||||
s += "#ifdef __cplusplus\n"
|
||||
s += "extern \"C\" {\n"
|
||||
s += "#endif\n\n"
|
||||
s += "#include <stdint.h>\n\n"
|
||||
s += gen_struct_def(cmd_list)
|
||||
s += "/* To avoid the volatile qualifier being a pain in the ass, the main loop\n"
|
||||
s += " * accesses the DataReal struct through this pointer. */\n"
|
||||
s += "extern volatile struct comm_data_t *Data;\n\n"
|
||||
s += "/* Parse a packet, update the struct, and send a reply. */\n"
|
||||
s += "void parse_packet(uint8_t *buf, uint16_t count);\n\n"
|
||||
for c in cmd_list:
|
||||
s += gen_send_proto(c) + "\n"
|
||||
s + gen_parse_proto(c) + "\n"
|
||||
s += gen_packing_protos()
|
||||
s += gen_build_str_dec()
|
||||
#s += "void send_packet(uint8_t *data, uint16_t count);\n\n"
|
||||
s += "#ifdef __cplusplus\n"
|
||||
s += "}\n"
|
||||
s += "#endif\n\n"
|
||||
s += "#endif\n"
|
||||
return s
|
||||
|
||||
def gen_struct_dec(cmd_list):
|
||||
s = "struct comm_data_t DataReal = {\n"
|
||||
for c in cmd_list:
|
||||
for i,d in zip(list(range(0, len(c["default"]))), c["default"]):
|
||||
s += "\t." + c["argument"][i][1] + " = " + d + ",\n"
|
||||
s += "};\n"
|
||||
s += "volatile struct comm_data_t *Data = &DataReal;\n"
|
||||
return s
|
||||
|
||||
def gen_source(cmd_list):
|
||||
s = "/* Warning: This file is automatically generated. Do not modify. */\n"
|
||||
s += "#include <stdint.h>\n"
|
||||
s += "#include <string.h>\n"
|
||||
s += "#include \"commgen.h\"\n\n"
|
||||
s += "#include \"comm.h\"\n\n"
|
||||
s += gen_struct_dec(cmd_list)
|
||||
for c in cmd_list:
|
||||
s += gen_parse_func(c) + "\n"
|
||||
s += gen_send_func(c, False) + "\n"
|
||||
s += gen_packing_funcs()
|
||||
s += gen_parse_packet_source(cmd_list)
|
||||
s += gen_build_str_def()
|
||||
return s
|
||||
|
||||
def gen_parse_packet_source(cmd_list):
|
||||
#TODO: check for count == 0
|
||||
"""Return a string containing the source code to the
|
||||
parse_packet(uint8_t *buf, uint16_t count)
|
||||
function, which parses a packet, updates values in the global Data structure,
|
||||
and dispatches a reply.
|
||||
The function relies on the following special functions:
|
||||
send_packet(uint8_t *data, uint16_t count) - send the given packet across the radio link.
|
||||
The send_packet() function must add a start and end byte and
|
||||
escape characters where necessary.
|
||||
as well as the send_* and parse_* functions."""
|
||||
s = ""
|
||||
s += "void parse_packet(uint8_t *buf, uint16_t count){\n"
|
||||
s += "\tuint8_t cmd = buf[0];\n"
|
||||
s += "\tswitch(cmd){\n"
|
||||
for c in cmd_list:
|
||||
s += "\t\t/* %s */\n"%(c["name"])
|
||||
s += "\t\tcase 0x%02X: /* (Write form) */\n"%c["code"]
|
||||
s += "\t\t\tparse_%s(buf, "%cannon_name(c["name"])
|
||||
add_trigger = False
|
||||
for a in c["argument"]:
|
||||
if a[0] == "*":
|
||||
s += "DataReal.%s, "%(a[1])
|
||||
add_trigger = True;
|
||||
else:
|
||||
s += "&(DataReal.%s), "%(a[1])
|
||||
s = s[0:-2] + ");\n"
|
||||
s += "\t\t\tbuf[0] = cmd;\n"
|
||||
s += "\t\t\tsend_packet(buf, 1);\n"
|
||||
if add_trigger:
|
||||
s += "\t\t\t%s_trigger();\n"%cannon_name(c["name"])
|
||||
s += "\t\t\tbreak;\n"
|
||||
|
||||
s += "\t\tcase 0x%02X: /* (Read form) */\n"%(c["code"] | 0x80)
|
||||
s += "\t\t\tsend_%s("%cannon_name(c["name"])
|
||||
for a in c["argument"]:
|
||||
s += "DataReal.%s, "%(a[1])
|
||||
s = s[0:-2] + ");\n"
|
||||
s += "\t\t\tbreak;\n"
|
||||
s += "\t\tdefault:\n"
|
||||
s += "\t\t\tbuf[0] = 0;\n"
|
||||
s += "\t\t\tsend_packet(buf, 1);\n"
|
||||
s += "\t\t\tbreak;\n"
|
||||
s += "\t}\n}\n"
|
||||
return s
|
||||
#TODO: writeable stuff
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 4:
|
||||
sys.stderr.write("error: wrong number of arguments. Expected path to spec file, source file, and header file.")
|
||||
with open(sys.argv[1], "r") as f:
|
||||
cmds = extract_table(f.read())
|
||||
with open(sys.argv[3], "w") as f:
|
||||
f.write(gen_header(cmds))
|
||||
with open(sys.argv[2], "w") as f:
|
||||
f.write(gen_source(cmds))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
289
OSU Robotics Club/Mars Rover 2016-2017/common/docparse.py
Normal file
289
OSU Robotics Club/Mars Rover 2016-2017/common/docparse.py
Normal file
@@ -0,0 +1,289 @@
|
||||
#!/usr/bin/env python3
|
||||
#Parses a markdown file (first command line argument)
|
||||
#containing a table with the header whose header starts with:
|
||||
#| Name | RW | Command Code
|
||||
import textwrap
|
||||
import sys
|
||||
import math
|
||||
|
||||
def extract_table(file_str):
|
||||
"""Extract the command table from the text of
|
||||
specification.md. Returns a list of dictionaries with the following
|
||||
members:
|
||||
"name" : str, name of command
|
||||
"rw" : str, contains r is command can be read, w if command can be written, - if neither
|
||||
"code" : int, command code (0-127)
|
||||
"argument" : list( #list of 0 or more tuples, each with the following structure:
|
||||
tuple(format code:str, #command format code, u8, i16, etc, or * to indicate a
|
||||
#a variable number of bytes controlled by the previous argument.
|
||||
name:str))
|
||||
"default" : list of default values of arguments, as strings
|
||||
"notes" : str, notes on command."""
|
||||
s = file_str.find("\n| Name | RW | Command Code")
|
||||
s+=1 #Skip over first newline
|
||||
e = file_str[s:].find("\n\n")
|
||||
if e < s:
|
||||
table_str = file_str[s:]
|
||||
else:
|
||||
table_str = file_str[s:s + e]
|
||||
commands = []
|
||||
for l in table_str.split("\n"):
|
||||
if len(l) == 0: continue
|
||||
if l[0] == "|":
|
||||
l = l[1:]
|
||||
col = [c.strip() for c in l.split("|")]
|
||||
if col[0][0:3] == "---": continue
|
||||
if col[0] == "Name": continue
|
||||
name = col[0]
|
||||
rw = col[1].lower()
|
||||
code = int(col[2], 0)
|
||||
notes = col[5]
|
||||
argument = []
|
||||
for a in [c.strip() for c in col[3].split(",")]:
|
||||
a = a.split(" ")
|
||||
if len(a) == 2:
|
||||
argument.append((a[0], a[1]))
|
||||
default = []
|
||||
if len(argument) > 0 and col[4].strip() != "-":
|
||||
for d in [c.strip() for c in col[4].split(",")]:
|
||||
default.append(d)
|
||||
if len(default) != len(argument) and len(default) != 0:
|
||||
sys.stderr.write("warning: default list length mismatch on command '%s'\n"%name)
|
||||
commands.append({
|
||||
"name" : name,
|
||||
"rw" : rw,
|
||||
"code" : code,
|
||||
"argument" : argument,
|
||||
"default" : default,
|
||||
"notes" : notes})
|
||||
return commands
|
||||
|
||||
def format_code_to_cstdint(fmt_name):
|
||||
"""Convert a format code (i8, u16, *, etc) to a c standard int (int8_t, uint16_t, uint8_t *)."""
|
||||
if fmt_name == "*":
|
||||
return "uint8_t *"
|
||||
elif fmt_name[0] == "i":
|
||||
return "int%s_t"%fmt_name[1:]
|
||||
elif fmt_name[0] == "u":
|
||||
return "uint%s_t"%fmt_name[1:]
|
||||
else:
|
||||
raise ValueError("Unknown format code \"%s\""%fmt_name)
|
||||
|
||||
def format_code_size(fmt_name):
|
||||
"""Return the number of bytes required for a type with the given format code."""
|
||||
if fmt_name == "*":
|
||||
return 255
|
||||
else:
|
||||
return int(fmt_name[1:])//8
|
||||
|
||||
def cannon_name(command):
|
||||
"""Given the name of a command, return the canonical function name
|
||||
(lowercase, with spaces converted to underscores)."""
|
||||
return command.lower().replace(" ", "_")
|
||||
|
||||
def gen_send_proto(cmd_dict):
|
||||
"""Given a command dictionary (from extract_table), return the prototype
|
||||
of a function that transmits the command."""
|
||||
cmd = cmd_dict
|
||||
|
||||
#Comment
|
||||
comment = "/* Note: %s */\n"%textwrap.fill(cmd["notes"], 80).replace("\n", "\n * ")
|
||||
|
||||
#Prototype
|
||||
need_trigger = False
|
||||
proto = "void "
|
||||
proto += "send_" + cannon_name(cmd["name"]) + "("
|
||||
for a in cmd["argument"]:
|
||||
proto += "" + format_code_to_cstdint(a[0]) + " " + a[1] + ", "
|
||||
if a[0] == "*": need_trigger = True
|
||||
proto = proto[0:-2] #Remove last commma
|
||||
proto += ");\n"
|
||||
|
||||
#Trigger
|
||||
trigger = ""
|
||||
if need_trigger:
|
||||
trigger = "void " + cannon_name(cmd["name"]) + "_trigger(void);\n"
|
||||
|
||||
return comment + proto + trigger
|
||||
|
||||
def gen_send_func(cmd_dict, write_mode):
|
||||
"""Given a command dictionary (from extract_table()), return a function that constructs
|
||||
a packet of that type. If write_mode is true, the command is transmitted in the
|
||||
write form; otherwise, the read form is used.
|
||||
|
||||
The function relies on the following functions:
|
||||
send_packet(uint8_t *data, uint16_t count) - send the given packet across the radio link.
|
||||
The send_packet() function must add a start and end byte and
|
||||
escape characters where necessary.
|
||||
pack*(uint8_t *data, uint16_t pos, uint*_t value) - pack the given value (where * = 8, 16, 32, or 64 bits)
|
||||
into the given buffer position in a little-endian format.
|
||||
memcpy(void *dest, void *src, uint16_t count) - copy bytes"""
|
||||
cmd = cmd_dict
|
||||
|
||||
#Comment
|
||||
comment = "/* Note: %s */\n"%textwrap.fill(cmd["notes"], 80).replace("\n", "\n * ")
|
||||
|
||||
#Prototype
|
||||
proto = "void "
|
||||
proto += "send_" + cannon_name(cmd["name"]) + "("
|
||||
for a in cmd["argument"]:
|
||||
proto += "" + format_code_to_cstdint(a[0]) + " " + a[1] + ", "
|
||||
proto = proto[0:-2] #Remove last commma
|
||||
proto += "){\n"
|
||||
|
||||
#Command
|
||||
command = "\tbuf[0] = 0x%X;\n"%(cmd["code"] | (int(not write_mode) << 7))
|
||||
|
||||
#Packet stuffing
|
||||
body = ""
|
||||
totalsize = 1 #current byte in packet buffer
|
||||
prev_a = None
|
||||
for a in cmd["argument"]:
|
||||
if a[0] == "*":
|
||||
if prev_a == None:
|
||||
raise ValueError("In command %s, variable-length argument used before length controlling argument.")
|
||||
body += "\tmemcpy(buf + b, %s, %s);\n"%(a[1], prev_a[1])
|
||||
body += "\tb += %s;\n"%prev_a[1]
|
||||
totalsize += 255
|
||||
else:
|
||||
s = format_code_size(a[0])
|
||||
body += "\tpack%d(buf, b, %s);\n"%(s*8, a[1])
|
||||
body += "\tb += %d;\n"%s
|
||||
totalsize += s
|
||||
prev_a = a
|
||||
|
||||
#Send command
|
||||
send = "\tsend_packet(buf, b);\n}\n"
|
||||
|
||||
#Declarations
|
||||
declarations = "\tuint16_t b = 1;\n\tuint8_t buf[%d];\n"%totalsize
|
||||
|
||||
return comment + proto + declarations + command + body + send
|
||||
|
||||
def gen_parse_proto(cmd_dict):
|
||||
"""Given a command dictionary (from extract_table()), return a function that
|
||||
Extracts a packet of that type from a buffer. The first byte in the buffer
|
||||
should be the command byte, escape characters must be removed, and it need
|
||||
not contain the end byte.
|
||||
|
||||
The generated function relies on the following functions:
|
||||
unpack*(uint8_t *data, uint16_t pos, uint*_t *result) - (where * = 8, 16, 32, etc.)
|
||||
unpack the little-endian value from the buffer and write it
|
||||
to a variable.
|
||||
memcpy(void *dest, void *src, uint16_t count) - copy bytes"""
|
||||
cmd = cmd_dict
|
||||
|
||||
#Comment
|
||||
comment = "/* Note: %s */\n"%textwrap.fill(cmd["notes"], 80).replace("\n", "\n * ")
|
||||
|
||||
#Prototype
|
||||
proto = "void "
|
||||
proto += "parse_" + cannon_name(cmd["name"]) + "("
|
||||
proto += "uint8_t *packet, "
|
||||
for a in cmd["argument"]:
|
||||
typename = "" + format_code_to_cstdint(a[0]).strip()
|
||||
if typename[-1] == "*":
|
||||
typename = typename[0:-1]
|
||||
proto += typename + " *" + a[1] + ", "
|
||||
proto = proto[0:-2] #Remove last commma
|
||||
proto += ";\n"
|
||||
|
||||
return comment + proto
|
||||
|
||||
def gen_parse_func(cmd_dict):
|
||||
"""Given a command dictionary (from extract_table()), return a function that
|
||||
Extracts a packet of that type from a buffer. The first byte in the buffer
|
||||
should be the command byte, escape characters must be removed, and it need
|
||||
not contain the end byte.
|
||||
|
||||
The generated function relies on the following functions:
|
||||
unpack*(uint8_t *data, uint16_t pos, uint*_t *result) - (where * = 8, 16, 32, etc.)
|
||||
unpack the little-endian value from the buffer and write it
|
||||
to a variable.
|
||||
memcpy(void *dest, void *src, uint16_t count) - copy bytes"""
|
||||
cmd = cmd_dict
|
||||
|
||||
#Comment
|
||||
comment = "/* Note: %s */\n"%textwrap.fill(cmd["notes"], 80).replace("\n", "\n * ")
|
||||
|
||||
#Prototype
|
||||
proto = "void "
|
||||
proto += "parse_" + cannon_name(cmd["name"]) + "("
|
||||
proto += "uint8_t *packet, "
|
||||
for a in cmd["argument"]:
|
||||
typename = "" + format_code_to_cstdint(a[0]).strip()
|
||||
if typename[-1] == "*":
|
||||
typename = typename[0:-1]
|
||||
proto += typename + " *" + a[1] + ", "
|
||||
proto = proto[0:-2] #Remove last commma
|
||||
proto += "){\n"
|
||||
|
||||
#Packet stuffing
|
||||
body = ""
|
||||
totalsize = 1 #current byte in packet buffer
|
||||
prev_a = None
|
||||
for a in cmd["argument"]:
|
||||
if a[0] == "*":
|
||||
if prev_a == None:
|
||||
raise ValueError("In command %s, variable-length argument used before length controlling argument."%cmd["name"])
|
||||
body += "\tmemcpy(%s, packet + b, *%s);\n"%(a[1], prev_a[1])
|
||||
body += "\tb += *%s;\n"%prev_a[1]
|
||||
totalsize += 255
|
||||
else:
|
||||
s = format_code_size(a[0])
|
||||
body += "\tunpack%d(packet, b, (uint%d_t *) %s);\n"%(s*8, s*8, a[1])
|
||||
body += "\tb += %d;\n"%s
|
||||
totalsize += s
|
||||
prev_a = a
|
||||
|
||||
#Declarations
|
||||
declarations = "\tuint16_t b = 1;\n"
|
||||
|
||||
return comment + proto + declarations + body + "}\n"
|
||||
|
||||
def gen_packing_protos():
|
||||
"""Return a string containing the prototypes of the functions:
|
||||
unpack*(uint8_t *data, uint16_t pos, uint*_t *result) - (where * = 8, 16, 32, etc.)
|
||||
unpack the little-endian value from the buffer and write it
|
||||
to a variable.
|
||||
pack*(uint8_t *data, uint16_t pos, uint*_t value) - pack the given value (where * = 8, 16, 32, or 64 bits)
|
||||
into the given buffer position in a little-endian format.
|
||||
where * = 8, 16, 32, and 64."""
|
||||
s = ""
|
||||
|
||||
for n in [8, 16, 32, 64]:
|
||||
s += "void pack%d(uint8_t *data, uint16_t pos, uint%d_t value);\n"%(n,n)
|
||||
|
||||
s += "void unpack%d(uint8_t *data, uint16_t pos, uint%d_t *result);\n"%(n,n)
|
||||
return s
|
||||
|
||||
def gen_packing_funcs():
|
||||
"""Return a string containing the source code to the functions:
|
||||
unpack*(uint8_t *data, uint16_t pos, uint*_t *result) - (where * = 8, 16, 32, etc.)
|
||||
unpack the little-endian value from the buffer and write it
|
||||
to a variable.
|
||||
pack*(uint8_t *data, uint16_t pos, uint*_t value) - pack the given value (where * = 8, 16, 32, or 64 bits)
|
||||
into the given buffer position in a little-endian format.
|
||||
where * = 8, 16, 32, and 64."""
|
||||
s = ""
|
||||
|
||||
for n in [8, 16, 32, 64]:
|
||||
s += "void pack%d(uint8_t *data, uint16_t pos, uint%d_t value){\n"%(n,n)
|
||||
for i in range(0, n//8):
|
||||
s += "\t*(data + pos + %d) = (value >> %d) & 0xFF;\n"%(i,i*8)
|
||||
s += "}\n"
|
||||
|
||||
s += "void unpack%d(uint8_t *data, uint16_t pos, uint%d_t *result){\n"%(n,n)
|
||||
s += "\t*result = "
|
||||
for i in range(0, n//8):
|
||||
s += "(((uint%d_t) *(data + pos + %d) << %d)) | "%(n,i, i * 8)
|
||||
s = s[0:-3] #trim last " | "
|
||||
s += ";\n}\n"
|
||||
return s
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
with open(sys.argv[1], "r") as f:
|
||||
cmds = extract_table(f.read())
|
||||
for c in cmds:
|
||||
print(c["argument"])
|
||||
408
OSU Robotics Club/Mars Rover 2016-2017/common/miniboard_gui.pyw
Normal file
408
OSU Robotics Club/Mars Rover 2016-2017/common/miniboard_gui.pyw
Normal file
@@ -0,0 +1,408 @@
|
||||
#!/usr/bin/env python
|
||||
Help = """Miniboard GUI Test Tool
|
||||
From a SPECIFICATION.md file (supplied as the first command-line
|
||||
argument), this program provides a GUI that can be used to get/set
|
||||
all registers on the miniboard."""
|
||||
|
||||
import sys
|
||||
import math
|
||||
import docparse #TODO: Add a try/catch and pop-up dialog.
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
import struct
|
||||
import serial
|
||||
import signal
|
||||
import os
|
||||
from serial.tools.list_ports import comports
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL) #Make Ctrl-C quit the program
|
||||
|
||||
#TODO: Check lengths everywhere, for validation
|
||||
#TODO: Add serial port selection
|
||||
#TODO: Add big pause/unpause buttons
|
||||
#TODO: Fix issue with spinbox that prevents typing 0 or -
|
||||
|
||||
#SerialPortPath = "/dev/ttyUSB0"
|
||||
SerialPortPath = comports()[0].device
|
||||
#SerialPortPath = "/dev/ttyACM0"
|
||||
|
||||
class MiniboardIO():
|
||||
"""Handles reading and writing from the miniboard."""
|
||||
path = SerialPortPath
|
||||
baud = 115200
|
||||
def __init__(self):
|
||||
os.system("stty -F %s -hupcl"%self.path)
|
||||
self.__tty = serial.Serial(port=self.path,
|
||||
baudrate=self.baud,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout=1)
|
||||
def calc_crc(self, body):
|
||||
body = [ord(b) for b in body]
|
||||
remainder = 0xFFFF
|
||||
for i in range(0, len(body)):
|
||||
remainder ^= body[i] << 8
|
||||
remainder &= 0xFFFF
|
||||
for bit in reversed(range(1,9)):
|
||||
if remainder & 0x8000:
|
||||
remainder = (remainder << 1) ^ 0x1021
|
||||
remainder &= 0xFFFF
|
||||
else:
|
||||
remainder <<= 1
|
||||
remainder &= 0xFFFF
|
||||
return remainder
|
||||
|
||||
#DON'T USE DIRECTLY RIGHT NOW! Use writeread()
|
||||
def write(self, packet_contents):
|
||||
"""Write a packet to the miniboard, inserting the header first."""
|
||||
print " send: ",
|
||||
packet_contents = "".join(packet_contents)
|
||||
plen = len(packet_contents) + 2
|
||||
crc = self.calc_crc(packet_contents)
|
||||
packet_contents = chr(0x01) + chr(plen) + chr(crc & 0xFF) + chr(crc >> 8) + packet_contents
|
||||
for c in packet_contents:
|
||||
print "0x%02X, "%ord(c),
|
||||
print "\n",
|
||||
self.__tty.write(packet_contents)
|
||||
|
||||
#DON'T USE DIRECTLY RIGHT NOW! Use writeread()
|
||||
def read(self):
|
||||
"""Read a packet from the miniboard and return the contents
|
||||
(with header removed).
|
||||
If the miniboard takes too long to reply, or if an error occurs,
|
||||
an exception will be raised. TODO: Exception type."""
|
||||
print " recv: ",
|
||||
reply = self.__tty.read(size=10000000)
|
||||
for c in reply:
|
||||
print "0x%02X, "%ord(c),
|
||||
print "\n",
|
||||
if len(reply) == 0:
|
||||
print "Error: no reply."
|
||||
return ""
|
||||
if reply[0] != chr(0x01):
|
||||
print "Error: missing start byte."
|
||||
return ""
|
||||
if len(reply) < 5:
|
||||
print "Error: no enough bytes."
|
||||
return ""
|
||||
if (len(reply) - 2) != ord(reply[1]):
|
||||
print "Error: length mismatch (header says %d, packet is %d)"%(ord(reply[1]), len(reply))
|
||||
return ""
|
||||
if self.calc_crc(reply[4:]) != (ord(reply[3])<<8 | ord(reply[2])):
|
||||
print "Error: CRC mismatch."
|
||||
return ""
|
||||
return "".join(reply[4:])
|
||||
|
||||
def writeread(self, packet_contents):
|
||||
"""Write a packet to the miniboard and return the reply."""
|
||||
#self.__tty = serial.Serial(port=self.path,
|
||||
#baudrate=self.baud,
|
||||
#parity=serial.PARITY_NONE,
|
||||
#stopbits=serial.STOPBITS_ONE,
|
||||
#bytesize=serial.EIGHTBITS,
|
||||
#timeout=0.1)
|
||||
self.write(packet_contents)
|
||||
reply = self.read()
|
||||
#self.__tty.close()
|
||||
return reply
|
||||
|
||||
def port_info(self):
|
||||
"""Return a tuple (serial_port_path, baud_rate)."""
|
||||
return (self.path, self.baud)
|
||||
|
||||
class RegisterController():
|
||||
"""Handles reading/writing a single register and controlling that
|
||||
register's UI widgets."""
|
||||
def setup(self, reg_dict, widgets, io):
|
||||
"""arg_row is the register specification from extract_table().
|
||||
widgets is a list of the widgets corresponding to each register,
|
||||
in the order they appear in the reg_dict.
|
||||
io is the MiniboardIO class to use."""
|
||||
self.reg = reg_dict
|
||||
self.widgets = widgets
|
||||
self.io = io
|
||||
self.fmtcodes = {"u8":"<B", "i8":"<b", "u16":"<H", "i16":"<h", "u32":"<I", "i32":"<i", "i64":"<Q", "i64":"<q"}
|
||||
def writefunc(self):
|
||||
"""Return a function (due to pyqt weirdness) for writing this register to the miniboard."""
|
||||
def func():
|
||||
print "Write reg 0x%02X (%s): "%(self.reg["code"], self.reg["name"])
|
||||
p = [chr(self.reg["code"] | 0x00)]
|
||||
for a,w,i in zip(self.reg["argument"], self.widgets, range(0, len(self.widgets))):
|
||||
if a[0] == "*":
|
||||
self.widgets[i-1].setValue(len(str(w.text()).decode('string_escape')))
|
||||
for a,w,i in zip(self.reg["argument"], self.widgets, range(0, len(self.widgets))):
|
||||
if a[0] != "*":
|
||||
value = w.value()
|
||||
p += list(struct.pack(self.fmtcodes[a[0]], value))
|
||||
else:
|
||||
p += str(w.text()).decode('string_escape')
|
||||
reply = self.io.writeread(p)
|
||||
return func
|
||||
|
||||
def readfunc(self):
|
||||
"""Return a function (due to pyqt weirdness) for reading this register from the miniboard."""
|
||||
def func():
|
||||
print "Read reg 0x%02X (%s): "%(self.reg["code"], self.reg["name"])
|
||||
p = [chr(self.reg["code"] | 0x80)]
|
||||
reply = self.io.writeread(p)
|
||||
if reply[0] != p[0]:
|
||||
print "Error: incorrect command code. Expected 0x%02X, got 0x%02X"%(ord(p[0]), ord(reply[0]))
|
||||
b = 1
|
||||
vs = []
|
||||
for a,w,i in zip(self.reg["argument"], self.widgets, range(0, len(self.widgets))):
|
||||
if a[0] != "*":
|
||||
s = struct.calcsize(self.fmtcodes[a[0]])
|
||||
value = struct.unpack(self.fmtcodes[a[0]], reply[b:b+s])[0]
|
||||
vs.append(value)
|
||||
b += s
|
||||
else:
|
||||
s = vs[i-1]
|
||||
value = reply[b:b+s]
|
||||
b+=s
|
||||
try:
|
||||
w.setValue(value)
|
||||
except:
|
||||
w.setText(value)
|
||||
return func
|
||||
|
||||
def update(self):
|
||||
"""Re-process widget data after editing."""
|
||||
|
||||
def horizontalLine():
|
||||
#From http://stackoverflow.com/questions/5671354/how-to-programmatically-make-a-horizontal-line-in-qt
|
||||
line = QFrame()
|
||||
line.setFrameShape(QFrame.HLine)
|
||||
line.setFrameShadow(QFrame.Sunken)
|
||||
return line
|
||||
|
||||
def argtype_minval(argtype):
|
||||
"""Return the minimum value for a numerical argument type."""
|
||||
if argtype[0] == "u":
|
||||
return 0
|
||||
elif argtype[0] == "i":
|
||||
return -(2**(int(argtype[1:])-1))
|
||||
else:
|
||||
return 0
|
||||
|
||||
def argtype_maxval(argtype):
|
||||
"""Return the maximum value for a numerical argument type."""
|
||||
if argtype[0] == "u":
|
||||
return 2**(int(argtype[1:])) - 1
|
||||
elif argtype[0] == "i":
|
||||
return 2**(int(argtype[1:])-1) - 1
|
||||
else:
|
||||
return 0
|
||||
|
||||
def argtype_minwidth(argtype):
|
||||
"""Return the minimum width of a spinbox for the given argument type."""
|
||||
m = 2.5 + math.ceil(math.log10(argtype_maxval(argtype)))
|
||||
return m * 9
|
||||
|
||||
class BigIntSpinBox(QAbstractSpinBox):
|
||||
"""From http://stackoverflow.com/questions/26841036/pyqt4-spinbox-with-64bit-integers"""
|
||||
def __init__(self, parent=None):
|
||||
super(BigIntSpinBox, self).__init__(parent)
|
||||
|
||||
self._singleStep = 1
|
||||
self._minimum = -18446744073709551616
|
||||
self._maximum = 18446744073709551615
|
||||
|
||||
self.lineEdit = QLineEdit(self)
|
||||
|
||||
rx = QRegExp("-[0-9]\\d{0,20}")
|
||||
validator = QRegExpValidator(rx, self)
|
||||
|
||||
self.lineEdit.setValidator(validator)
|
||||
self.setLineEdit(self.lineEdit)
|
||||
|
||||
def value(self):
|
||||
try:
|
||||
return int(self.lineEdit.text())
|
||||
except:
|
||||
raise
|
||||
return 0
|
||||
|
||||
def setValue(self, value):
|
||||
if self._valueInRange(value):
|
||||
self.lineEdit.setText(str(value))
|
||||
|
||||
def stepBy(self, steps):
|
||||
self.setValue(self.value() + steps*self.singleStep())
|
||||
|
||||
def stepEnabled(self):
|
||||
return self.StepUpEnabled | self.StepDownEnabled
|
||||
|
||||
def setSingleStep(self, singleStep):
|
||||
assert isinstance(singleStep, int)
|
||||
# don't use negative values
|
||||
self._singleStep = abs(singleStep)
|
||||
|
||||
def singleStep(self):
|
||||
return self._singleStep
|
||||
|
||||
def minimum(self):
|
||||
return self._minimum
|
||||
|
||||
def setMinimum(self, minimum):
|
||||
assert isinstance(minimum, int) or isinstance(minimum, long)
|
||||
self._minimum = minimum
|
||||
|
||||
def maximum(self):
|
||||
return self._maximum
|
||||
|
||||
def setMaximum(self, maximum):
|
||||
assert isinstance(maximum, int) or isinstance(maximum, long)
|
||||
self._maximum = maximum
|
||||
|
||||
def _valueInRange(self, value):
|
||||
if value >= self.minimum() and value <= self.maximum():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
AutoreadTimer = None
|
||||
|
||||
def setup(window, spec_table, io):
|
||||
ww = QWidget(window)
|
||||
window.setWindowTitle("Miniboard GUI Test Tool")
|
||||
flayout = QFormLayout()
|
||||
vlayout = QVBoxLayout()
|
||||
read_list = []
|
||||
write_list = []
|
||||
autoread_list = []
|
||||
for r in spec_table:
|
||||
label = QLabel(r["name"])
|
||||
hl = QHBoxLayout()
|
||||
annotated_args = []
|
||||
control_widgets = []
|
||||
rc = RegisterController()
|
||||
for a,i in zip(r["argument"],range(0, len(r["argument"]))):
|
||||
a = [a[0], a[1], False] #Type, Name, Is_Size_Controller
|
||||
annotated_args.append(a)
|
||||
if a[0] == "*":
|
||||
annotated_args[i-1][2] = True
|
||||
|
||||
for a,i in zip(annotated_args,range(0, len(annotated_args))):
|
||||
vl = QVBoxLayout()
|
||||
if a[0] == "*":
|
||||
widget = QLineEdit()
|
||||
elif a[0] == "u64" or a[0] == "i64" or a[0] == "u32":
|
||||
widget = BigIntSpinBox()
|
||||
widget.setMinimum(argtype_minval(a[0]))
|
||||
widget.setMaximum(argtype_maxval(a[0]))
|
||||
widget.setMinimumSize(QSize(argtype_minwidth(a[0]), 0))
|
||||
else:
|
||||
widget = QSpinBox()
|
||||
widget.setMinimum(argtype_minval(a[0]))
|
||||
widget.setMaximum(argtype_maxval(a[0]))
|
||||
widget.setMinimumSize(QSize(argtype_minwidth(a[0]), 0))
|
||||
if len(r["default"]) == 0:
|
||||
widget.setValue(0)
|
||||
else:
|
||||
widget.setValue(int(r["default"][i]))
|
||||
if a[2] or "w" not in r["rw"]:
|
||||
widget.setEnabled(False)
|
||||
control_widgets.append(widget)
|
||||
subtitle = QLabel(a[1])
|
||||
subtitle.setFont(QFont("", 8))
|
||||
vl.addWidget(widget)
|
||||
vl.addWidget(subtitle)
|
||||
|
||||
hl.addLayout(vl)
|
||||
rc.setup(r, control_widgets, io)
|
||||
rbtn = QToolButton()
|
||||
readfunc = rc.readfunc()
|
||||
writefunc = rc.writefunc()
|
||||
rbtn.setText("Read")
|
||||
rbtn.pressed.connect(readfunc)
|
||||
wbtn = QToolButton()
|
||||
wbtn.setText("Write")
|
||||
wbtn.pressed.connect(writefunc)
|
||||
ar_check = QCheckBox()
|
||||
|
||||
if "r" not in r["rw"]:
|
||||
rbtn.setEnabled(False)
|
||||
ar_check.setEnabled(False)
|
||||
else:
|
||||
read_list.append(readfunc)
|
||||
autoread_list.append((ar_check, readfunc))
|
||||
if "w" not in r["rw"]:
|
||||
wbtn.setEnabled(False)
|
||||
else:
|
||||
write_list.append(writefunc)
|
||||
|
||||
bvl = QVBoxLayout()
|
||||
hbvl = QHBoxLayout()
|
||||
hbvl.addWidget(ar_check)
|
||||
hbvl.addWidget(rbtn)
|
||||
hbvl.addWidget(wbtn)
|
||||
bvl.addLayout(hbvl)
|
||||
bvl.addSpacing(21)
|
||||
hl.addStretch(1)
|
||||
hl.addLayout(bvl)
|
||||
flayout.addRow(label, hl)
|
||||
gh = QHBoxLayout()
|
||||
gh.addWidget(QLabel("Port: %s Baud: %d"%io.port_info()))
|
||||
gh.addStretch(1)
|
||||
|
||||
def read_all():
|
||||
for f in read_list:
|
||||
f()
|
||||
|
||||
def write_all():
|
||||
for f in write_list:
|
||||
f()
|
||||
|
||||
def autoread():
|
||||
for t in autoread_list:
|
||||
if t[0].isChecked():
|
||||
print "autoread"
|
||||
t[1]()
|
||||
|
||||
rbtn = QToolButton()
|
||||
rbtn.setText("Read All")
|
||||
rbtn.pressed.connect(read_all)
|
||||
wbtn = QToolButton()
|
||||
wbtn.setText("Write All")
|
||||
wbtn.pressed.connect(write_all)
|
||||
ar_lbl = QLabel("Auto Read");
|
||||
|
||||
timer = QTimer(ar_lbl)
|
||||
timer.timeout.connect(autoread)
|
||||
timer.start(2000);
|
||||
|
||||
gh.addWidget(ar_lbl)
|
||||
gh.addWidget(rbtn)
|
||||
gh.addWidget(wbtn)
|
||||
vlayout.addLayout(gh)
|
||||
vlayout.addWidget(horizontalLine())
|
||||
scrollarea = QScrollArea()
|
||||
scrollarea.setWidgetResizable(True)
|
||||
inner = QFrame(scrollarea)
|
||||
inner.setLayout(flayout)
|
||||
scrollarea.setWidget(inner)
|
||||
vlayout.addWidget(scrollarea)
|
||||
ww.setLayout(vlayout)
|
||||
window.setCentralWidget(ww)
|
||||
sh = flayout.sizeHint()
|
||||
window.resize(QSize(sh.width() + 50, 700))
|
||||
|
||||
def get_specpath():
|
||||
"""Return the path to the specification file,
|
||||
using a hardcoded default, command-line argument, pop-up dialog, etc."""
|
||||
return "SPECIFICATION.md"
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
w = QMainWindow()
|
||||
spec = None
|
||||
with open(get_specpath(), "r") as f:
|
||||
spec = docparse.extract_table(f.read());
|
||||
io = MiniboardIO()
|
||||
setup(w, spec, io)
|
||||
|
||||
w.show()
|
||||
sys.exit(app.exec_())
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python
|
||||
Help = """Miniboard Test-Drive
|
||||
From a SPECIFICATION.md file (supplied as the first command-line
|
||||
argument), this program provides takes in joystick input and drives
|
||||
the rover."""
|
||||
import sys
|
||||
import math
|
||||
import docparse #TODO: Add a try/catch and pop-up dialog.
|
||||
from PyQt4.QtGui import *
|
||||
from PyQt4.QtCore import *
|
||||
import struct
|
||||
import serial
|
||||
import signal
|
||||
import os
|
||||
import pygame
|
||||
import time
|
||||
import struct
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL) #Make Ctrl-C quit the program
|
||||
|
||||
SerialPortPath = "/dev/ttyUSB0"
|
||||
|
||||
class MiniboardIO():
|
||||
"""Handles reading and writing from the miniboard."""
|
||||
path = SerialPortPath
|
||||
baud = 115200
|
||||
def __init__(self):
|
||||
os.system("stty -F %s -hupcl"%self.path)
|
||||
self.__tty = serial.Serial(port=self.path,
|
||||
baudrate=self.baud,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout=0.5)
|
||||
def calc_crc(self, body):
|
||||
body = [ord(b) for b in body]
|
||||
remainder = 0xFFFF
|
||||
for i in range(0, len(body)):
|
||||
remainder ^= body[i] << 8
|
||||
remainder &= 0xFFFF
|
||||
for bit in reversed(range(1,9)):
|
||||
if remainder & 0x8000:
|
||||
remainder = (remainder << 1) ^ 0x1021
|
||||
remainder &= 0xFFFF
|
||||
else:
|
||||
remainder <<= 1
|
||||
remainder &= 0xFFFF
|
||||
return remainder
|
||||
|
||||
#DON'T USE DIRECTLY RIGHT NOW! Use writeread()
|
||||
def write(self, packet_contents):
|
||||
"""Write a packet to the miniboard, inserting the header first."""
|
||||
print " send: ",
|
||||
packet_contents = "".join(packet_contents)
|
||||
plen = len(packet_contents) + 2
|
||||
crc = self.calc_crc(packet_contents)
|
||||
packet_contents = chr(0x01) + chr(plen) + chr(crc & 0xFF) + chr(crc >> 8) + packet_contents
|
||||
for c in packet_contents:
|
||||
print "0x%02X, "%ord(c),
|
||||
print "\n",
|
||||
self.__tty.write(packet_contents)
|
||||
|
||||
#DON'T USE DIRECTLY RIGHT NOW! Use writeread()
|
||||
def read(self):
|
||||
"""Read a packet from the miniboard and return the contents
|
||||
(with header removed).
|
||||
If the miniboard takes too long to reply, or if an error occurs,
|
||||
an exception will be raised. TODO: Exception type."""
|
||||
print " recv: ",
|
||||
reply = self.__tty.read(size=10000000)
|
||||
for c in reply:
|
||||
print "0x%02X, "%ord(c),
|
||||
print "\n",
|
||||
if len(reply) == 0:
|
||||
print "Error: no reply."
|
||||
return ""
|
||||
if reply[0] != chr(0x01):
|
||||
print "Error: missing start byte."
|
||||
return ""
|
||||
if len(reply) < 5:
|
||||
print "Error: no enough bytes."
|
||||
return ""
|
||||
if (len(reply) - 2) != ord(reply[1]):
|
||||
print "Error: length mismatch (header says %d, packet is %d)"%(ord(reply[1]), len(reply))
|
||||
return ""
|
||||
if self.calc_crc(reply[4:]) != (ord(reply[3])<<8 | ord(reply[2])):
|
||||
print "Error: CRC mismatch."
|
||||
return ""
|
||||
return "".join(reply[4:])
|
||||
|
||||
def writeread(self, packet_contents):
|
||||
"""Write a packet to the miniboard and return the reply."""
|
||||
#self.__tty = serial.Serial(port=self.path,
|
||||
#baudrate=self.baud,
|
||||
#parity=serial.PARITY_NONE,
|
||||
#stopbits=serial.STOPBITS_ONE,
|
||||
#bytesize=serial.EIGHTBITS,
|
||||
#timeout=0.1)
|
||||
self.write(packet_contents)
|
||||
reply = self.read()
|
||||
#self.__tty.close()
|
||||
return reply
|
||||
|
||||
def port_info(self):
|
||||
"""Return a tuple (serial_port_path, baud_rate)."""
|
||||
return (self.path, self.baud)
|
||||
|
||||
def horizontalLine():
|
||||
#From http://stackoverflow.com/questions/5671354/how-to-programmatically-make-a-horizontal-line-in-qt
|
||||
line = QFrame()
|
||||
line.setFrameShape(QFrame.HLine)
|
||||
line.setFrameShadow(QFrame.Sunken)
|
||||
return line
|
||||
|
||||
def setup(window, spec_table, io):
|
||||
ww = QWidget(window)
|
||||
window.setWindowTitle("Miniboard GUI Test Drive")
|
||||
|
||||
|
||||
def get_specpath():
|
||||
"""Return the path to the specification file,
|
||||
using a hardcoded default, command-line argument, pop-up dialog, etc."""
|
||||
return "SPECIFICATION.md"
|
||||
|
||||
#def main():
|
||||
#app = QApplication(sys.argv)
|
||||
#w = QMainWindow()
|
||||
#spec = None
|
||||
#with open(get_specpath(), "r") as f:
|
||||
#spec = docparse.extract_table(f.read());
|
||||
#io = MiniboardIO()
|
||||
#setup(w, spec, io)
|
||||
|
||||
#w.show()
|
||||
#sys.exit(app.exec_())
|
||||
|
||||
def set_drive_power(io, left, right):
|
||||
"""Left and right are floats from -1 to 1."""
|
||||
l = int(127 * left)
|
||||
r = int(127 * right)
|
||||
s = "".join([struct.pack('b', i) for i in [0x10, l, l, l, r, r, r]])
|
||||
io.writeread(s)
|
||||
|
||||
def set_swerve_mode(io, mode):
|
||||
if mode == 1 or mode == 2:
|
||||
print "Swerve mode: ", mode
|
||||
s = "".join([struct.pack('b', i) for i in [0x11, mode]])
|
||||
io.writeread(s)
|
||||
|
||||
def get_joystick(joystick):
|
||||
"""Return a tuple (l,r) of motor powers from the joystick."""
|
||||
x = joystick.get_axis(0)
|
||||
y = joystick.get_axis(1)
|
||||
#print x, y
|
||||
#theta = math.atan2(y, x)
|
||||
#rad = math.sqrt(x**2 + y**2)
|
||||
#print theta, rad
|
||||
#if(rad > 1):
|
||||
#rad = 1
|
||||
#l=x
|
||||
#r=-y
|
||||
return (x,y)
|
||||
|
||||
def main():
|
||||
with open(get_specpath(), "r") as f:
|
||||
spec = docparse.extract_table(f.read());
|
||||
io = MiniboardIO()
|
||||
pygame.init()
|
||||
pygame.joystick.init()
|
||||
joysticks = [pygame.joystick.Joystick(x) for x in range(pygame.joystick.get_count())]
|
||||
j = joysticks[0]
|
||||
print j.get_name()
|
||||
j.init()
|
||||
oldl = 0
|
||||
oldr = 0
|
||||
while(True):
|
||||
pygame.init()
|
||||
pygame.event.get()
|
||||
l,r = get_joystick(j)
|
||||
#print "Motor power L=", l, "R=", r
|
||||
if(oldl != l or oldr != r):
|
||||
set_drive_power(io, l, r)
|
||||
oldl = l
|
||||
oldr = r
|
||||
if int(j.get_button(6)) == 1:
|
||||
set_swerve_mode(io, 2)
|
||||
elif int(j.get_button(7)) == 1:
|
||||
set_swerve_mode(io, 1)
|
||||
else:
|
||||
set_swerve_mode(io, 0)
|
||||
time.sleep(30)
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,19 @@
|
||||
import sys
|
||||
|
||||
to_parse = ""
|
||||
|
||||
def __parse_and_show_science_data(string):
|
||||
split_data = string[3:].split(",")
|
||||
TempC = float(split_data[0])
|
||||
Moisture = float(split_data[2])
|
||||
Cond = float(split_data[4])
|
||||
PermR = float(split_data[6])
|
||||
PermI = float(split_data[8])
|
||||
Salinity = Cond * 6.4
|
||||
|
||||
print("Temp in C: " + str(TempC))
|
||||
print("Moisture: " + str(Moisture * 100) + "%")
|
||||
print("Salinity: " + str(Salinity) + "g/L")
|
||||
|
||||
if __name__ == '__main__':
|
||||
__parse_and_show_science_data(sys.argv[1])
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
import math
|
||||
import struct
|
||||
import serial
|
||||
import signal
|
||||
import os
|
||||
import time
|
||||
import struct
|
||||
|
||||
SerialPath="/dev/ttyUSB0"
|
||||
|
||||
class SoilSensor():
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
self.__tty = serial.Serial(port=self.path,
|
||||
baudrate=9600,
|
||||
parity=serial.PARITY_NONE,
|
||||
stopbits=serial.STOPBITS_ONE,
|
||||
bytesize=serial.EIGHTBITS,
|
||||
timeout=0.2)
|
||||
def set_mode_rx(self):
|
||||
"""Set the transceiver to receive mode."""
|
||||
#Set DTR for receive mode, clear for transmit
|
||||
self.__tty.setDTR(True)
|
||||
|
||||
def set_mode_tx(self):
|
||||
"""Set the transceiver to transmit mode."""
|
||||
self.__tty.setDTR(False)
|
||||
|
||||
def send_command(self, addr_str, command_str):
|
||||
"""Send a command to the soil sensor."""
|
||||
self.set_mode_tx()
|
||||
time.sleep(.04)
|
||||
self.__tty.write(addr_str + command_str + "\r")
|
||||
self.__tty.flush()
|
||||
time.sleep(.05)
|
||||
self.__tty.write("\n")
|
||||
time.sleep(.005)
|
||||
self.set_mode_rx()
|
||||
reply = self.__tty.read(size=10000000)
|
||||
return reply
|
||||
|
||||
def set_data(self, addr_str, command_str, data_str):
|
||||
"""Set data in the soil sensor."""
|
||||
self.set_mode_tx()
|
||||
time.sleep(.04)
|
||||
self.__tty.write(addr_str + command_str + "=" + data_str + "\r\n")
|
||||
self.__tty.flush()
|
||||
time.sleep(.05)
|
||||
self.__tty.write("\n")
|
||||
time.sleep(.005)
|
||||
self.set_mode_rx()
|
||||
reply = self.__tty.read(size=10000000)
|
||||
return reply
|
||||
|
||||
def get_data(self, addr_str, command_str):
|
||||
"""Get data from the sensor, returning the data.
|
||||
command_str is the two-character string."""
|
||||
self.set_mode_tx()
|
||||
time.sleep(.04)
|
||||
self.__tty.write(addr_str + command_str + "=?" + "\r")
|
||||
self.__tty.flush()
|
||||
time.sleep(.05)
|
||||
self.__tty.write("\n")
|
||||
time.sleep(.005)
|
||||
self.set_mode_rx()
|
||||
reply = self.__tty.read(size=10000000)
|
||||
return reply
|
||||
|
||||
def get_measurement(self, addr_str):
|
||||
Moisture = float(data[2])
|
||||
Cond = float(data[4])
|
||||
"""Take and return a soil measurement."""
|
||||
addr = addr_str
|
||||
self.send_command(addr, "TR")
|
||||
time.sleep(1)
|
||||
data = self.send_command(addr, "T3")
|
||||
print data
|
||||
data = data[3:]
|
||||
data = data.split(",")
|
||||
print "Raw Values (reading set 3):", data
|
||||
TempC = float(data[0])
|
||||
PermR = float(data[6])
|
||||
PermI = float(data[8])
|
||||
Salinity = Cond * 6.4
|
||||
return {"TempC":TempC, "Moisture":Moisture, "Salinity":Salinity}
|
||||
def main():
|
||||
s = SoilSensor(SerialPath)
|
||||
addr = s.get_data("///", "SN")[0:3]
|
||||
s.set_data(addr, "PE", "1")
|
||||
time.sleep(1)
|
||||
while True:
|
||||
print s.get_measurement(addr)
|
||||
print ""
|
||||
time.sleep(10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user