# Copyright 2024 N4CNR (Richard Neese)
# Permission is granted to use, copy, modify, and distribute this software under the MIT License.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
"""
Brand: Transcend Radios
Line: TrailWave
Product: SSB-144
"""
import time
import board
import busio
import digitalio
import analogio
import displayio
import terminalio
import rotaryio
from adafruit_debouncer import Debouncer
from adafruit_display_text import label
from adafruit_display_shapes.rect import Rect
import adafruit_ili9341
import pwmio
import adafruit_si5351
# Release any displays that may be in use
displayio.release_displays()
# Constants
CW_TIMEOUT = 600 # milliseconds
TAP_HOLD_MILLIS = 2000 # milliseconds
MODE_NORMAL = 0
MODE_CALIBRATE = 1
ITU_REGIONS = {
1: {"low": 144000000, "high": 144500000},
2: {"low": 144000000, "high": 144500000},
3: {"low": 144000000, "high": 144500000},
}
# Initial VFO settings
class VFO:
def __init__(self, region):
self.freq_a = ITU_REGIONS[region]["low"]
self.freq_b = ITU_REGIONS[region]["low"]
self.active_vfo = 'A'
self.frequency = self.freq_a
def get_active_freq(self):
return self.freq_a if self.active_vfo == 'A' else self.freq_b
def set_active_freq(self, freq):
if self.active_vfo == 'A':
self.freq_a = freq
else:
self.freq_b = freq
vfo = VFO(2)
# Initial frequency settings
base_tune = 144000000
step_size = 10000 # Default step size
itu_region = 1
rit_on = False
rit_offset = 0.0 # RIT offset in kHz
is_usb = True
# TX/RX and CW status
in_tx = False
key_down = False
cw_timeout = 0
mode = MODE_NORMAL
# Initialize I2C bus and SI5351
i2c = busio.I2C(scl=board.GP5, sda=board.GP4)
si5351 = adafruit_si5351.SI5351(i2c)
# Display setup
tft_clk, tft_mosi, tft_reset = board.GP18, board.GP19, board.GP16
tft_dc, tft_cs = board.GP15, board.GP17
spi = busio.SPI(clock=tft_clk, MOSI=tft_mosi)
# Rotary encoder setup
freq_encoder = rotaryio.IncrementalEncoder(board.GP11, board.GP12)
rit_encoder = rotaryio.IncrementalEncoder(board.GP7, board.GP8)
# Debouncer setup for buttons
buttons = {
"cal_btn": Debouncer(digitalio.DigitalInOut(board.GP0)),
"funct_btn": Debouncer(digitalio.DigitalInOut(board.GP1)),
"freq_enc_btn": Debouncer(digitalio.DigitalInOut(board.GP13)),
"itu_btn": Debouncer(digitalio.DigitalInOut(board.GP6)),
"ptt_btn": Debouncer(digitalio.DigitalInOut(board.GP2)),
"rit_enc_btn": Debouncer(digitalio.DigitalInOut(board.GP9)),
"step_btn": Debouncer(digitalio.DigitalInOut(board.GP3))
}
for button in buttons.values():
button.direction = digitalio.Direction.INPUT
button.pull = digitalio.Pull.UP
# TX/RX pin setup tx/rx relay
tx_rx_pin = digitalio.DigitalInOut(board.GP20)
tx_rx_pin.direction = digitalio.Direction.OUTPUT
tx_rx_pin.value = False # Ensure the pin is initially off
# PTT pin setup
ptt_pin = digitalio.DigitalInOut(board.GP10)
ptt_pin.direction = digitalio.Direction.INPUT
ptt_pin.pull = digitalio.Pull.UP
# Setup analog input pins
analog_pins = {
"A0": analogio.AnalogIn(board.A0),
"A1": analogio.AnalogIn(board.A1),
"A2": analogio.AnalogIn(board.A2),
}
# PWM for CW key and tone
cw_key = pwmio.PWMOut(board.GP21, frequency=800, duty_cycle=0)
cw_tone = pwmio.PWMOut(board.GP22, frequency=800, duty_cycle=0)
# Display bus setup
display_bus = displayio.FourWire(spi, command=tft_dc, chip_select=tft_cs, reset=tft_reset)
display = adafruit_ili9341.ILI9341(display_bus, width=160, height=128, rotation=90)
# Display groups and elements
display_group = displayio.Group()
display.show(display_group)
line_labels = [
label.Label(terminalio.FONT, text="", color=0xFFFF00, x=0, y=10),
label.Label(terminalio.FONT, text="", color=0xFFFF00, x=0, y=30),
label.Label(terminalio.FONT, text="", color=0xFFFF00, x=0, y=50),
label.Label(terminalio.FONT, text="", color=0xFFFF00, x=0, y=70)
]
for line in line_labels:
display_group.append(line)
blue_bar = Rect(0, 110, 160, 18, fill=0x0000FF)
transmitting_label = label.Label(terminalio.FONT, text="Transmitting", color=0xFFFFFF, x=5, y=100)
display_group.append(blue_bar)
display_group.append(transmitting_label)
# S-meter setup
s_meter_rect = Rect(5, 5, 5, 10, fill=0x00FF00) # S-meter rectangle (initially 0 width)
display_group.append(s_meter_rect)
def read_analog_pins():
"""Reads and prints the values of the analog input pins."""
for pin_name, pin in analog_pins.items():
print(f"{pin_name}: {pin.value}")
# Display functions
def update_display():
"""Updates the display with the current frequency, mode, status."""
vfo_text = f"VFO {vfo.active_vfo}: " # Display active VFO
display_text = f"{vfo_text}{'USB' if is_usb else 'LSB'} {vfo.get_active_freq() // 10000}.{vfo.get_active_freq() % 10000:04d}"
display_text += " TX" if in_tx else " +R" if rit_on else ""
if rit_on:
display_text += f" RIT:{rit_offset:+.1f}kHz"
line_labels[0].text = display_text
line_labels[1].text = f"Step Size: {step_size} Hz"
line_labels[2].text = f"ITU Region: {itu_region}"
blue_bar.hidden = not in_tx
transmitting_label.hidden = not in_tx
def update_s_meter():
"""Updates the S-meter display based on the current signal strength, scaled from 0 to 10."""
signal_strength = analog_pins["A0"].value
s_meter_value = signal_strength / 65535 * 10 # Scale from 0 to 10
s_meter_width = int(s_meter_value * 16) # Scale width to match 0 to 160 pixel width for display
s_meter_rect.width = s_meter_width
s_meter_rect.x = (160 - s_meter_width) // 2 # Center the S-meter horizontally
line_labels[2].text = f"S: {s_meter_value:.1f}" # Optionally display the S-meter value
# Frequency and Tuning Functions
def enforce_band_limits(freq):
"""Enforces the ITU region-specific band limits on the frequency."""
region_limits = ITU_REGIONS[itu_region]
return max(region_limits["low"], min(freq, region_limits["high"]))
def set_frequency(f):
"""Sets the frequency on the Si5351 chip and updates the display."""
f = enforce_band_limits(f)
vfo.set_active_freq(f)
si5351.clock_0.frequency = f * 100 # Set the frequency on the Si5351 clock
update_display()
def do_tuning():
"""Tunes the frequency using the rotary encoder."""
new_freq = base_tune + (freq_encoder.position * step_size)
if rit_on and not in_tx:
new_freq += int(rit_offset * 1000)
set_frequency(new_freq)
time.sleep(0.2)
def change_step_size():
"""Changes the step size for frequency tuning."""
global step_size
step_size = {1000: 10000, 10000: 100000, 100000: 1000}.get(step_size, 10000)
line_labels[1].text = f"Step Size: {step_size}"
time.sleep(0.5)
update_display()
def adjust_rit():
"""Adjusts the RIT (Receiver Incremental Tuning) offset."""
global rit_offset
rit_offset = rit_encoder.position / 10.0 # Adjust RIT by 0.1 kHz per step
update_display()
def check_cw_timeout():
"""Checks if CW has timed out and returns to RX mode."""
global in_tx, key_down
if in_tx and time.monotonic() - cw_timeout > CW_TIMEOUT:
in_tx = False
key_down = False
cw_key.duty_cycle = 0
tx_rx_pin.value = False # Switch back to RX mode
update_display()
def check_buttons():
"""Checks the status of all buttons and handles their actions."""
global in_tx, is_usb, rit_on, mode, itu_region
buttons["cal_btn"].update()
buttons["freq_enc_btn"].update()
buttons["rit_enc_btn"].update()
buttons["funct_btn"].update()
buttons["itu_btn"].update()
buttons["step_btn"].update()
if buttons["freq_enc_btn"].fell:
# Toggle between USB and LSB
is_usb = not is_usb
update_display()
time.sleep(0.2)
if buttons["funct_btn"].fell:
# Enter or exit calibration mode
mode = MODE_CALIBRATE if mode == MODE_NORMAL else MODE_NORMAL
update_display()
time.sleep(0.2)
if buttons["itu_btn"].fell:
# Change ITU region
itu_region = (itu_region % 3) + 1
update_display()
time.sleep(0.2)
if buttons["rit_enc_btn"].fell:
# Toggle RIT on/off
rit_on = not rit_on
update_display()
time.sleep(0.2)
if buttons["step_btn"].fell:
# Change step size
change_step_size()
def transmit():
"""Handles the PTT (Push-to-Talk) for transmission."""
global in_tx, cw_timeout
if ptt_pin.value == False: # Active low PTT button
in_tx = True
cw_timeout = time.monotonic()
tx_rx_pin.value = True # Switch to TX mode
update_display()
else:
in_tx = False
tx_rx_pin.value = False # Switch back to RX mode
update_display()
# Main loop
while True:
do_tuning()
adjust_rit()
check_buttons()
check_cw_timeout()
update_s_meter()
transmit()
time.sleep(0.1)