Sending 5 bytes over Serial Port

Hi Folks, Python Newb, here.

I’ve gone in circles for weeks and I guess I am daft. I have a radio that uses RS-232 for communication. It expects 5 bytes to be sent; where the last byte is the Command byte (always Hex) and the first four are Parameters which may be Hex or BCD. If no Parameters are required, the bytes need to be padded (with anything) to create 5 bytes. The easiest command is xx xx xx xx 85; where there are no Parameters and the Command is 0x85. The port is set correctly for the radio (4800, 8, N, 2). I wrote a “Listen” app in LabVIEW to use on another COM Port to display what is sent and it confirms I send the Literal String "Null Null Null Null Ellipsis and the Hex 0x00,0x00,0x00,0x85 for all variations of sending code below.

Using Python 3.10 with pyserial inside PySrcripter 4.1.1.0 x64 on a Win 10 machine I have tried using ser.write(hex_string) with the data:
hex_string = bytes.fromhex(“0000000085”)
hex_string = bytes([0x00,0x00,0x00,0x00,0x85])
hex_string = b’\x00\x00\x00\x00\x85’
hex_string = b’\00\00\00\00\85’
hex_string = b’00000000\85’
hex_string = b’00000000133’

I obviously do not know what I am doing. Please, what am I missing about the two functions used above? Is either the correct function to insert into ser.write()?

What is the correct format to send 5 bytes of data using ser.write()? May I have an example for the xx xx xx xx 85 command?

Does Python send anything additional that I don’t know about that might confuse the radio?

I am reasonably sure if I can get the radio to respond to the above command, I will be able to expand to more complicated commands. Your help is certainly appreciated.

Thank you,
John

It could be expecting in the reverse byte order. Try sending

hex_string = bytes.fromhex(“8500000000”)

These are OK:

hex_string = bytes.fromhex("0000000085") # OK
hex_string = bytes([0x00,0x00,0x00,0x00,0x85]) # OK
hex_string = b'\x00\x00\x00\x00\x85' # OK

Here it thinks that they are octal (base 8). b’\00’ is OK, but ‘8’ is not a valid digit for octal:

hex_string = b'\00\00\00\00\85' # Contains invalid octal digit

The non-escaped digits are treated as characters. b’0’ is equivalent to b’\x41’ (ord('0') == 0x41 == 65). ‘8’ is not a valid digit for octal:

hex_string = b'00000000\85' # Contains invalid octal digit

All of these are characters, so it’s equivalent to b’\x41\x41\x41\x41\x41\x41\x41\x41\x43\x43\x43’:

hex_string = b'00000000133' # All literal characters

You haven’t said exactly what the problem is.

On a related note, you might need to add ser.flush() to ensure that the data is sent out promptly, otherwise it might just sit in the output buffer.

To Dan, the radio expects the last byte to be the OpCode (in Hex). For the heck of it, I sent:
b’\0x85,\0x85\0x85\0x85\0x85’ with no response (the radio should copy VFO-A to VFO-B).

To Matthew, you confirmed for me that 3 versions are correct. I don’t want to get off topic, but my understanding was the escape character indicated a Hexadecimal number immediately following and that number did not require the Hex prefix. I guess I missed that part in the Python Help for Byte classes. I learn something every day.

You wrote:

Are you saying that the ASCII decimal equivalent to the ASCII Hex number is a valid string to send and I should expect the same results (that is, whether I use b’Decimal or b’Hex?

I inserted ser.flush() (with no argument) on the line following ser.write(). I don’t know if that is where it goes nor whether it requires an argument. The radio did not respond.

To your question as to what the problem is, I cannot get the radio to respond to the simplest command - Copy VFO-A frequency to VFO-B. The Command is 5 bytes with the first 4 padded (with anything) and the 5th byte being a Hex OpCode byte (85). I have a QT/C++ exe that does this and proves the radio works. The author insists it is my formatting that is awry but he is not a Python guy. The Listen app I wrote indicates that the Literal String and the Numeric equivalents (decimal and hex) as well as the number of bytes sent are the same using his code (which works) and any of mine, which don’t. I’ve explored whether there is an extra byte hanging at the end of all versions and there isn’t. The radio’s manual expressly calls for 5 bytes with no Termination Characters.

My Listen app returns what is sent but I must ask anyway; as the command byte is 0x85, does pyserial have a problem with extended ASCII?

It has to be me. Any other thoughts, Folks?

Thanks for the replies.
John

b'\10' contains octal and is bytes([8]).

b'\x10' contains hexadecimal and is bytes([16]).

b'0' is a bytestring of length 1. It’s equivalent to bytes([ord('0')]), which is bytes([0x41]), or bytes([65]).

b'00' is a bytestring of length 2. It’s equivalent to bytes([ord('0'), ord('0')]), which is bytes([0x41, 0x41]), or bytes([65, 65]).

How are you configuring the serial? Can you share some code?

Is the manual available online?

Thank you for your patience, Mathew.

The manual can be found here (it is safe): https://www.radioamatore.info/attachments/852_FT-1000MP_Mark-V_user_manual.pdf. The pertinent section is manual pages 86 through 97 (PDF 88-99). The command I am trying to dupicate is on page 96 (98) labeled “[A>B]”.

I took your 3 acceptable options and populate a variable to send with one of the three.

#-------------------------------------------------------------------------------
# Name:        Send Command
# Purpose:
#
# Author:      John
#
# Created:     25/06/2025
# Copyright:   (c) HollyJohn 2025
# Licence:     <your licence>
#-------------------------------------------------------------------------------

def main():
    pass

if __name__ == '__main__':
    main()

import serial
import time


ser = serial.Serial(
    port='COM5',  # Or '/dev/ttyUSB0' on Linux/macOS
    baudrate=4800,
    parity=serial.PARITY_NONE,
    stopbits=serial.STOPBITS_TWO,
    bytesize=serial.EIGHTBITS,
    timeout=1  # Timeout in seconds
)

try:
    # Open the serial port
    if not ser.isOpen():
        ser.open()
    print(f"Serial port {ser.port} opened successfully.")


    #Option 1
    option1 = bytes.fromhex("0000000085")

    #Option 2
    option2 = b'\x00\x00\x00\x00\x85'

    #Option 3
    option3 = bytes([0x00,0x00,0x00,0x00,0x85])

    #Populate Variable With Option
    data_to_send = option3

    # Send the data\
    print (data_to_send)
    ser.write(data_to_send)
    ser.flush()
    # Wait for a short period to allow the device to respond
    time.sleep(0.1)

    # Read response (optional)
    if ser.in_waiting > 0:
        response = ser.read(ser.in_waiting)
        print(f"Received response: {response.hex()}")

except serial.SerialException as e:
    print(f"Error opening or communicating with serial port: {e}")

finally:
    # Close the serial port
    if ser.isOpen():
        ser.close()
        print(f"Serial port {ser.port} closed.")

That is it. Thank you for your interest in helping.

John

I can’t see any obvious problem.

Is that command expected to return a response? Some of the other commands say that they return a response, but 0x85 doesn’t.

Try reading the status flags with 0xFA. That says that it returns something.

Do you know how long the radio will take to return a response? If 0.1 seconds isn’t enough, you could be missing any response.

You could drop the time.sleep(0.1) and just read the expected number of bytes (5 or 6 for reading the status flags). That’ll be safe because you specified a timeout. You can always reduce it later if you find that it’s way too long.

Hi Matthew,

I was afraid you might find nothing wrong - that tells you how long I have chasing my tail.

The 0x85 returns nothing. It Copies the contents of VFO-A (main receiver) to VFO-B (sub-receiver). I chose this because it requires no arguments. Again, the kind man who spent some time on my dilemma wrote a QT/C++ app to achieve this operation successively. I can pull it up and it will perform the operation. His code is wrapped in an exe so I have no access to what he did except read the string and numbers he sent in my LabVIEW Read app.

Originally, we wasted quite a bit of time on getting a version to work only to find-out that the UART in the radio was bad (1.6k Ohms between RX and TX pins). I replaced the UART and his code then worked. We both agreed that if his code worked, then the radio is responding correctly since there is no way the radio could know the difference between flavors of software. Perhaps we are wrong on this issue, but I am not an I/O expert. For the Hell of me, I cannot understand why his exe works and everything I (and this Group) have thrown at it fails.

I did as you suggested and removed time.sleep(0.1) and if ser.in_waiting > 0: (to force a read) and nothing is returned.

I am sorry to impose on you, but if you have any more thoughts, I am open…

Thanks,
John

1 Like

On page 86 it says “Each time a command instruction is being received from the computer via the CAT port, the “CAT” indicator appears in the display, then turns off afterward.”

Does that happen with the other software?

Does that happen with the Python code?

Have you tried reading the status flags?

Good Morning, Matthew.

Yes, the CAT annunciator lights momentarily with the software that works. I get no response with mine.

I sent the FA command and received nothing from buffer.

I might mention that the working software does not work on the first button click. It however works on every subsequent click until the exe is closed. I asked the author about this behavior, but he did not address it, and I was not going to push him. Another quirk is my software is unable to access the port until his exe is closed. I am presuming he leaves the port open and closes it when the exe closes.

Thanks,
John

It’s interesting that the working software doesn’t work on the first button click.

If that’s the case, and you’re sending only 1 command from Python, then, well, this is no worse! :slight_smile:

Does the working software have a button to connect/disconnect? If you’re not telling it to disconnect, then it’ll remain connected.

Here’s a simple Python GUI that you can try:

# A simple GUI for testing talking to a FT-1000MP_Mark-V radio via the CAT/serial port.
#
from serial import Serial, SerialException
from tkinter.messagebox import showerror
import tkinter as tk

# The port to use.
DEFAULT_PORT = "COM5"

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Radio GUI")

        # Add a frame for the buttons.
        button_frame = tk.Frame(self)
        button_frame.pack(side=tk.TOP, fill=tk.X)

        # Add the "Connect" button.
        self.connect_button = tk.Button(button_frame, text="Connect", padx=5, pady=5, command=self.on_connect)
        self.connect_button.pack(side=tk.LEFT)

        # Add the "Clear log" button.
        self.clear_log_button = tk.Button(button_frame, text="Clear log", padx=5, pady=5, command=self.on_clear_log)
        self.clear_log_button.pack(side=tk.LEFT)

        # Add the "A -> B" button.
        self.copy_vfo_a_button = tk.Button(button_frame, text="A -> B", padx=5, pady=5, command=lambda: self.send("A -> B", bytes.fromhex("00 00 00 00 85")))
        self.copy_vfo_a_button.pack(side=tk.LEFT)

        # Add the "Read status flags" button.
        self.read_status_flags_button = tk.Button(button_frame, text="Read status flags", padx=5, pady=5, command=lambda: self.send("Read status flags", bytes.fromhex("00 00 00 00 FA")))
        self.read_status_flags_button.pack(side=tk.LEFT)

        # Add a frame to display the log.
        response_frame = tk.Frame(self)
        response_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)

        # Add a title label for the log frame.
        tk.Label(response_frame, text="Log").pack(side=tk.TOP)

        # Add a listbox with a scrollbar to display the log.
        self.log_listbox = tk.Listbox(response_frame)
        self.log_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar = tk.Scrollbar(response_frame, orient=tk.VERTICAL, command=self.log_listbox.yview)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.log_listbox.configure(yscrollcommand=scrollbar.set)

        # The serial port and response buffer.
        self.serial_port = None
        self.response = b""

        # Disable the buttons initially.
        self.enable_buttons(False)

        # Periodically check for a response.
        self.on_tick()

    def on_tick(self):
        if self.serial_port is not None:
            # Read any (additional) response from the serial port. We want to append to the current response, if any.
            if self.response:
                self.log_listbox.delete(tk.END)

            self.response += self.serial_port.read(self.serial_port.in_waiting)

            if self.response:
                self.log_listbox.insert(tk.END, self.response.hex(" ").upper())

        # Schedule the next check.
        self.after(200, self.on_tick)

    def on_connect(self):
        if self.serial_port is None:
            # Try to connect to the serial port.
            try:
                self.serial_port = Serial(port=DEFAULT_PORT, baudrate=4800, parity="N", stopbits=2, bytesize=8)
            except SerialException as ex:
                showerror(self.title(), str(ex))
            else:
                # We connected successfully, so enable the buttons.
                self.enable_buttons(True)
                self.connect_button["text"] = "Disconnect"
        else:
            # Disconnect from the serial port and disable the buttons.
            self.serial_port.close()
            self.serial_port = None
            self.connect_button["text"] = "Connect"
            self.enable_buttons(False)

    def on_clear_log(self):
        self.log_listbox.delete(0, tk.END)
        self.response = b""

    def send(self, description, data):
        if self.serial_port is None:
            return

        # We're going to send a command, so log the action and clear the response buffer.
        self.log_listbox.insert(tk.END, f"[{description}]")
        self.response = b""

        try:
            self.serial_port.write(data)
            self.serial_port.flush()
        except SerialException as ex:
            showerror(self.title(), str(ex))
            self.serial_port.close()
            self.serial_port = None
            self.connect_button["text"] = "Connect"
            self.enable_buttons(False)

    def enable_buttons(self, enable):
        new_state = tk.NORMAL if enable else tk.DISABLED
        self.copy_vfo_a_button["state"] = new_state
        self.read_status_flags_button["state"] = new_state
        self.hello_button["state"] = new_state

App().mainloop()

Hello,

I reviewed the user manual. Specifically on page 86, it states that the instructions from the computer to the transceiver are to be sent in 5-byte blocks. However, I don’t think that they should be sent together as one block but rather as of 5 independent bytes sent in series, with the last byte being the op-code. Note that it states "with up to 200 ms between each byte". This implies separate byte messages. Your script, as written, sends 5 bytes as one message. The figure below is from the manual (the figure below the paragraph I drew using MS Publisher as a visual aid)

Just one other point, you are sending messages as hex. Should the messages be encoded to binary?

Unless there is a typo and they meant with up to 200 ms between each block?

… by the way, this part of your script is not needed as it doesn’t do anything - you can delete it:

A 5-byte block is a series of 5 bytes sent in series!

The part that says “with up to 200 ms between each byte” is specifying the maximum length of time that can elapse between the bytes of a block for it to be recognised as a single command.

Yes, the script sends 5 bytes as one message; it’s a series of 5 bytes, each sent as 1 start bit, 8 data bytes, and 2 stop bits.

Thank you, Matthew. You are going the extra mile to help a stranger. I hope there is a resolution as I don’t want to wear you out.

I copied your code to a new module and got this error:

  File "<module1>", line 72, in __init__    self.enable_buttons(False)  File "<module1>", line 135, in enable_buttons    self.hello_button["state"] = new_state  File "C:\Users\HollyJohn\AppData\Local\Programs\Python\Python310\lib\tkinter\__init__.py", line 2383, in __getattr__    return getattr(self.tk, attr)AttributeError: '_tkinter.tkapp' object has no attribute 'hello_button'

I expect this is a GUI with a button?

To your question about the ‘working’ exe:
No, it has no buttons to open nor close a port. It simply has a button labelled “A to B” that executes the copy command. It is strange that the first click does nothing. I opened his exe and connected the serial cable to another port and using my Listen app confirmed it produced a command on first click. Without closing the exe, I moved the serial cable to the radio and it took two clicks for the radio to respond, after which it worked every time. Very strange.

I am going through an email where he has supplied his C++ code for formatting the string to send. He is using enums to retrieve OpCodes. He appears to be messing with the LOCK function - you can lock or unlock a VFO so that you cannot inadvertently change frequencies. This feature can be done either through the front panel (which is OFF) or via a CAT command. But I am stumbling over the C++ (of which I haven’t used in 25 years and is more foreign to me than Python) and his inclusion of this command and his use of a mysterious m which has something to do with the enum. He kind of jumps about and it is in separate emails of a thread so it is quite confusing. I am reasonably sure that an Unlock command does not have to be sent, but for the heck of it, I sent that command before the 0x85 command - still with no success.

Oops! Please delete that line. That’s just something I did for testing.

It might be that when you moved the serial cable, that first command failed because the hardware detected that something had changed, so it discarded the data and then re-connected to the new device.

When I connect to different devices, my PC uses a different COM port for each one, even when I’m using the same physical USB port.

Send to the COM port. That now fails. Find a device on another COM port. Connect to that.

The software might be connecting automatically on the first command and then remaining connected.

Yes, I understand what bytes and bits are. But if I take them literally, that is how I am interpreting their statement. A timing diagram and sample code goes a long way but apparently this company is on a budget. Somewhere in their website they allude to scrounging the internet for code examples such as from ham radio forums and the like (apparently users shouldn’t look to the manufacturer to provide that).

Per the argument in this paragraph, they’ve chosen not to include any SW examples (in other words, they’ve opted for the mantra of "let's not try at all").

For example, for a ti microcontoller, they provide a timing diagram for a I2C comm protocol - they should have at least provided a timing diagram such as this as a reference:

Hi Matthew,

OK, I remarked the line out and your GUI appeared. Clicking the “Connect” button tosses this error:

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Users\HollyJohn\AppData\Local\Programs\Python\Python310\lib\tkinter\__init__.py", line 1921, in __call__
    return self.func(*args)
  File "<module1>", line 100, in on_connect
  File "<module1>", line 135, in enable_buttons
  File "C:\Users\HollyJohn\AppData\Local\Programs\Python\Python310\lib\tkinter\__init__.py", line 2383, in __getattr__
    return getattr(self.tk, attr)
AttributeError: '_tkinter.tkapp' object has no attribute 'hello_button'

Clicking any other button does not throw an error.

Aren’t you glad you got involved in my rabbit hole?

John

If you removed the line, how is it causing a problem?

Also, I’m puzzled that the traceback says that enable_buttons is on line 135. What I posted had only 119 lines!

Put what I posted in a file, but delete line 117 (the line that says “self.hello_button…”).

The last line (line 118) says App().mainloop()

Save and run.

Duh. I remarked-out the wrong line. The GUI appears and the “Connect” button changes its text to “Disconnect”. It is connecting to the Com port as it should (I tried connecting with the exe and could not) and the “Disconnect” works too (same test).

The “A->B” and “Read status flags” both log but the radio sits there like useless employee.

Thoughts?

And thanks,
John
P.S. Your effort is a great learning experience for me. I was wondering how to produce a pop-up…

Can you look in Window’s Device Manager for “Ports”. Does only “COM5” appear when you connect the radio?