Tag: MacOS

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

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

    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