Winlink on Apple Silicon: The Files, Scripts and One-Click Launcher

Home amateur radio station showing an HF transceiver with waterfall display, hand microphone, notebook, and laptop running Winlink and ARDOP for digital radio messaging.

Written by

in

  1. Step by step – 1st Class
  2. Step-by-Step – 2nd Class
  3. Amateur Radio Codes of Conduct
  4. Step-by-Step – 3rd Class
  5. Step-by-Step – Class 4a
  6. Step-by-Step – Classes 4b & 4c
  7. Step-by-Step—Class 5
  8. Step-by-Step—Class 6
  9. Step-by-Step—Class 7
  10. Step-by-Step—Class 8a
  11. Step-by-Step—Class 8b
  12. Step-by-Step—Class 9
  13. Step-by-Step—Class 10
  14. Step-by-step—Class 11
  15. A Modern Code of Conduct and Ethics for Amateur Radio
  16. Step-by-step—Class 12
  17. Step-by-step—Class 13
  18. Step-by-step—Class 14
  19. Step-by-step—Class 15
  20. Step-by-step—Class 16
  21. Step-by-step—Class 17
  22. Reflection: My Amateur Radio Journey So Far
  23. Authorized to Transmit
  24. Starting the Next Chapter: Advanced & CW
  25. Making Winlink, Pat, ARDOP and a Yaesu FTdx10 Work on an Apple Silicon Mac
  26. Winlink on Apple Silicon: The Files, Scripts and One-Click Launcher

In my previous post, I told the story of getting Winlink, Pat, ARDOP and my Yaesu FTDX10 working reliably on an Apple Silicon Mac.

That post was about the journey: the false starts, the serial-port detective work, the PTT problem, the ARDOP audio problem, the first successful RMS connection, and the eventual realization that the cleanest architecture was much simpler than the one I started with.

This post is the practical companion.

If you want to build something similar, this is where I’m putting the pieces together in one place: the launcher, the shell scripts, the Core Location/GPS bridge, and the configuration details that made the whole thing work.

A quick warning before we start: do not blindly copy my device names and assume they will match yours. The FTdx10 serial ports on your Mac will almost certainly have different names. Treat the files here as a working template, not a universal drop-in installer.

What this setup does

My finished launcher brings up the entire Winlink station in one go.

The architecture looks like this:

macOS Core Location
        ↓
CoreLocationCLI
        ↓
NMEA generator
        ↓
socat pseudo-terminal
        ↓
gpsd
        ↓
Pat
        │
        ├── rigctld ── CAT ── FTDX10 Enhanced COM
        │
        └── ardopcf ── RTS ── FTDX10 Standard COM
                       │
                       └── USB AUDIO CODEC

The FTdx10’s USB interface exposes two virtual COM ports, and that is what makes this arrangement so clean. Yaesu assigns the Enhanced COM Port to CAT communications and the Standard COM Port to transmit control, such as PTT and digital operation.

In my installation:

/dev/cu.usbserial-00F490390

is the Enhanced CAT port, while:

/dev/cu.usbserial-00F490391

is the Standard TX-control port.

Your names will be different.

Required software

The setup assumes the following are installed:

Pat
Hamlib
ardopcf
CoreLocationCLI
gpsd
socat

On my machine, Homebrew handles most of the supporting utilities:

brew install hamlib
brew install corelocationcli
brew install gpsd
brew install socat

Pat and ardopcf need to be installed separately according to their respective project instructions.

You can confirm that Hamlib recognizes the FTdx10 with:

rigctl -l | grep -i FTDX10

My installation reports:

1042  Yaesu  FTDX-10  Stable

Finding your FTdx10 serial ports

Start with:

ls /dev/cu.* /dev/tty.* | grep -Ei 'usb|serial'

You should see two serial devices associated with the FTdx10.

To identify the Enhanced CAT port, test each candidate with:

rigctl \
  -m 1042 \
  -r /dev/cu.YOURPORT \
  -s 38400

At the Rig command: prompt, enter:

f

If Hamlib returns the radio’s current frequency, you have found the CAT port.

On my Mac, that test returned:

Frequency: 14009914

which matched the frequency displayed on the radio and confirmed that /dev/cu.usbserial-00F490390 was the correct CAT device.

The remaining FTdx10 USB serial device is then the Standard TX‑control port.

The radio setting that matters

On the FTdx10, PTT must be configured so that the USB virtual COM port can control transmit.

The relevant setting is:

RPTT SELECT

and for my installation it is set to:

RTS

Yaesu documents RTS/DTR as the USB virtual-COM transmit-control option for digital operation.

Pat configuration

Pat uses rigctld for tuning and ardopcf for the modem.

The important Hamlib section of my Pat configuration is:

"hamlib_rigs": {
  "FTdx10": {
    "network": "tcp",
    "address": "127.0.0.1:4532",
    "VFO": ""
  }
}

The ARDOP section is:

"ardop": {
  "addr": "127.0.0.1:8515",
  "arq_bandwidth": {
    "Forced": false,
    "Max": 2000
  },
  "connect_requests": 10,
  "rig": "FTdx10",
  "ptt_ctrl": false,
  "beacon_interval": 0,
  "cwid_enabled": true
}

The important detail is:

"ptt_ctrl": false

Pat is not responsible for PTT in this setup.

ardopcf owns the Standard USB serial port and uses RTS directly.

That division of labour is what finally made the setup reliable.

Starting rigctld

The working Hamlib command is:

rigctld \
  -m 1042 \
  -r /dev/cu.usbserial-00F490390 \
  -s 38400 \
  -T 127.0.0.1 \
  -t 4532

That gives Pat a network-accessible CAT interface at:

127.0.0.1:4532

Starting ARDOP

This is the working ardopcf command on my station:

build/macos/ardopcf \
  -p /dev/cu.usbserial-00F490391 \
  --hostcommands "MYCALL VE3ZDN;DRIVELEVEL 2" \
  8515 \
  "USB AUDIO  CODEC" \
  "USB AUDIO  CODEC"

A few notes are worth calling out.

First, -p assigns the Standard serial port to PTT using RTS.

Second, I explicitly set:

MYCALL VE3ZDN

and:

DRIVELEVEL 2

at startup.

Third, the FTdx10 USB audio device on my Mac appears as:

USB AUDIO  CODEC

with two spaces between AUDIO and CODEC.

That detail caused more head-scratching than it deserved.

Your audio device name may differ.

The Core Location to GPSd bridge

Pat can use GPSd to update its locator automatically.

The Mac does not expose Core Location as a conventional serial GPS receiver, so I added a small bridge.

The flow is:

CoreLocationCLI
      ↓
Python NMEA generator
      ↓
socat PTY pair
      ↓
gpsd
      ↓
Pat

The Python script is called:

~/bin/corelocation-nmea

and looks like this:

#!/usr/bin/env python3

import json
import subprocess
import time
import datetime
import sys


def checksum(sentence):
    c = 0
    for ch in sentence:
        c ^= ord(ch)
    return f"{c:02X}"


def dd_to_nmea(value, is_lat):
    value = float(value)

    hemi = (
        ("N" if value >= 0 else "S")
        if is_lat
        else ("E" if value >= 0 else "W")
    )

    value = abs(value)

    deg = int(value)
    minutes = (value - deg) * 60

    if is_lat:
        field = f"{deg:02d}{minutes:07.4f}"
    else:
        field = f"{deg:03d}{minutes:07.4f}"

    return field, hemi


def emit(sentence):
    full = f"${sentence}*{checksum(sentence)}"
    print(full, flush=True)


while True:
    try:
        result = subprocess.run(
            ["CoreLocationCLI", "--json"],
            capture_output=True,
            text=True,
            check=True,
            timeout=10
        )

        data = json.loads(result.stdout)

        lat, ns = dd_to_nmea(data["latitude"], True)
        lon, ew = dd_to_nmea(data["longitude"], False)

        now = datetime.datetime.now(datetime.timezone.utc)

        hhmmss = now.strftime("%H%M%S")
        ddmmyy = now.strftime("%d%m%y")

        rmc = (
            f"GPRMC,{hhmmss}.00,A,"
            f"{lat},{ns},{lon},{ew},"
            f"0.0,0.0,{ddmmyy},,,A"
        )

        emit(rmc)

        gga = (
            f"GPGGA,{hhmmss}.00,"
            f"{lat},{ns},{lon},{ew},"
            f"1,08,1.0,0.0,M,0.0,M,,"
        )

        emit(gga)

    except Exception as e:
        print(
            f"# CoreLocation error: {e}",
            file=sys.stderr,
            flush=True
        )

    time.sleep(2)

I create the pseudo-terminal pair with socat, feed the NMEA output into one side, and give the other side to real gpsd.

Pat is configured with:

"gpsd": {
  "enable_http": false,
  "allow_forms": false,
  "use_server_time": false,
  "update_locator": true,
  "addr": "127.0.0.1:2947"
}

When everything is working, Pat logs something like:

Locator changed from EN93tj to EN93TJ

which confirms that it has accepted the live position from GPSd.

The one-click launcher

I wrapped everything in a small AppleScript application called:

Winlink ARDOP.app

The AppleScript itself is intentionally simple:

set choice to button returned of (display dialog ¬
    "Winlink ARDOP station control" buttons {"Cancel", "Stop", "Start"} ¬
    default button "Start" with title "Winlink ARDOP")

if choice is "Start" then

    display notification "Starting Winlink ARDOP station…" ¬
        with title "Winlink ARDOP"

    do shell script "/bin/zsh -lc '$HOME/bin/start-winlink-ardop' 2>&1"

    display dialog "Winlink ARDOP startup completed." ¬
        buttons {"OK"} default button "OK" ¬
        with title "Winlink ARDOP"

else if choice is "Stop" then

    display notification "Stopping Winlink ARDOP station…" ¬
        with title "Winlink ARDOP"

    do shell script "/bin/zsh -lc '$HOME/bin/stop-winlink-ardop' 2>&1"

    display dialog "Winlink ARDOP station shut down." ¬
        buttons {"OK"} default button "OK" ¬
        with title "Winlink ARDOP"

end if

The application is really just a friendly wrapper around:

~/bin/start-winlink-ardop

and:

~/bin/stop-winlink-ardop

That means the logic stays in ordinary shell scripts where it is easy to inspect and troubleshoot.

What the startup script does

The startup script:

  • verifies the required programs are installed;
  • verifies both FTDX10 serial devices exist;
  • creates the GPS pseudo-terminal pair;
  • starts the Core Location NMEA generator;
  • starts GPSd;
  • starts rigctld;
  • checks that CAT can actually read the FTDX10 frequency;
  • starts ardopcf;
  • opens both USB audio channels;
  • enables RTS PTT;
  • sets my callsign and TX drive level;
  • starts Pat;
  • waits for the required TCP ports to come up;
  • opens the ARDOP and Pat browser interfaces;
  • writes logs for troubleshooting.

The shutdown script stops everything in reverse order and cleans up the GPS pseudo-terminal links.

That last point is important.

During development, I discovered that stale Pat or ARDOP processes could leave things in a strange state. A clean shutdown and restart usually restored normal operation immediately.

For that reason, I prefer the launcher to own the whole stack rather than casually reusing old processes.

Logging

The launcher writes its logs to:

~/winlink-launcher-logs

including:

pat.log
ardopcf.log
rigctld.log
gpsd.log
corelocation-nmea.log
socat-gps.log

These logs are extremely useful.

For example, this:

Mod4FSKDataAndPlay() called when not TXEnabled. Ignoring.

tells a very different story from:

Sending Frame Type ConReq2000M

and once you start seeing things such as:

DataACK
BREAK
4FSK...

you are looking at real two-way ARDOP traffic.

Download files

I’m making the following files available with this post:

start-winlink-ardop
stop-winlink-ardop
corelocation-nmea
Winlink ARDOP AppleScript source
sample Pat configuration

I recommend reading through every script before running it.

At minimum, you will need to change:

callsign
CAT serial device
PTT serial device
audio device name

and possibly:

CAT baud rate
ARDOP drive level
paths to Pat and ardopcf

depending on your installation.

A few final cautions

This setup works on my FTdx10 and my Apple Silicon Mac.

It is not a turnkey universal installer.

In particular, do not assume my:

DRIVELEVEL 2

is correct for your radio.

Set transmit audio conservatively and verify that you are not overdriving the transmitter.

Also remember that successful software configuration does not guarantee a successful RMS connection.

My first confirmed connection was to K5RAV, about 2,500 km away.

A later successful connection was to N0DAJ, about 3,000 km away.

Several much closer stations failed simply because 20-metre propagation favoured the longer paths at the time.

Once you know the radio is really transmitting valid ARDOP frames, start considering propagation before tearing your software apart again.

That lesson would have saved me several hours.

Final result

At this point, I can click one icon, press Start, and end up with:

live Mac location
GPSd
Pat
rigctld
ARDOP
FTDX10 CAT control
FTDX10 RTS PTT
USB modem audio

all running together.

And most importantly, I’ve connected successfully to multiple Winlink RMS stations over HF.

If this collection of scripts saves another Mac user even half the time I spent figuring it out, publishing them will have been worthwhile.

73,

Doug Nix, VE3ZDN
Kitchener, Ontario
EN93tj

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.