benlink icon indicating copy to clipboard operation
benlink copied to clipboard

RadioController.send_tnc_data with VR-N76

Open amirdahal opened this issue 8 months ago • 2 comments

I have observed a behaviour while sending an APRS message using RadioController.send_tnc_data. This method works well while working with the BTECH UV-PRO, but I am facing trouble when working with the VR-N76. I saw in the README that the package has not been tested with the VR-N76, which might be the issue.

The behaviour isn't consistent, as there are times when I can send one packet once in a while, but 95% of the time, the transmission fails. I will include the code for encoding and decoding the packet.

Encoder

def ax25_callsign(call, ssid=0, last=False):
    call = call.upper().ljust(6)  # Ensure call is exactly 6 characters
    encoded = bytes([(ord(c) << 1) for c in call[:6]])  # Shift ASCII values
    encoded += bytes([0x60 | (ssid << 1) | (0x01 if last else 0x00)])  # SSID + last bit
    return encoded

def build_aprs_message(dest_call, src_call, digipeaters, recipient, message):
    # Encode AX.25 callsigns
    dest = ax25_callsign(dest_call, 0, last=False)
    src_ssid = int(src_call.split('-')[1]) if '-' in src_call else 0
    src = ax25_callsign(src_call.split('-')[0], src_ssid, last=(len(digipeaters) == 0))

    # Encode digipeaters
    digis = b""
    for i, digi in enumerate(digipeaters):
        call, ssid = digi.split('-') if '-' in digi else (digi, 0)
        last = (i == len(digipeaters) - 1)  # Only last digipeater has last bit set
        digis += ax25_callsign(call, int(ssid), last=last)

    # Construct APRS payload correctly (ensure exactly 9 characters for recipient)
    recipient = recipient.ljust(9)[:9]  # Left-align and enforce 9-char limit
    aprs_payload = f":{recipient}:{message}"  # Ensure only two colons

    control_pid = b'\x03\xf0'  # UI-frame, no layer 3 protocol
    return dest + src + digis + control_pid + aprs_payload.encode()

Decoder

def ax25_decode(addr_bytes):
        callsign = ''
        for i in range(6):
            char = (addr_bytes[i] >> 1) & 0x7F
            if char != 0x20:
                callsign += chr(char)
        ssid = (addr_bytes[6] >> 1) & 0x0F
        return f"{callsign}-{ssid}" if ssid else callsign

def parse_aprs(data):
        source_addr = ax25_decode(data[7:14])
        dest_addr = ax25_decode(data[:7])

        digipeaters = []
        offset = 14
        while offset + 7 < len(data) and not (data[offset] & 1):
            digipeaters.append(ax25_decode(data[offset:offset+7]))
            offset += 7

        payload_index = data.index(b'\x03\xf0') + 2
        aprs_payload = data[payload_index:].decode(
            'ascii', errors='ignore').strip()
        aprs_packet = f"{source_addr}>{dest_addr}" + \
            (f",{','.join(digipeaters)}" if digipeaters else "") + \
            f":{aprs_payload}"
       return aprs_packet

Usage

frame = build_aprs_message("APN000", "NOCALL-1", ["WIDE1-1", "WIDE2-2"], "DESCAL-1", "Hello there")
print(frame)
parsed = parse_aprs(frame)
print(parsed)

The above algorithm is implemented to work for both UV-PRO and VR-N76, but its only working for one.

amirdahal avatar Apr 12 '25 07:04 amirdahal

There's a chance the VR-N76 is doing something related to this bug: https://github.com/khusmann/benlink/issues/1

Two things to try to see if this is related:

  • After it fails, if you immediately try re-sending the packet, does it succeed? (by "immediately" I mean resend within 0.1s)
  • If you connect to the audio channel, does it start working reliably?

And for general context:

  • What is your firmware version on the N76?
  • Do you get an error (e.g. "invalid state") when the transmission fails? Or does it simply not transmit?

khusmann avatar Apr 13 '25 22:04 khusmann

firmware version 0.9.3 just got out of beta.

gretel avatar Apr 14 '25 16:04 gretel