Inhoudsopgave:

https://github.com/PA3EFR?tab=repositories

Cluster warnings. Sound with CMD window.

    • pip install playsound

import socket
import re
import winsound  # Voor Windows; gebruik 'playsound' voor andere systemen.

# Configuratie: pas deze waarden aan
HOST = "www.db0erf.de"  # Vervang door de host-IP van het DXCluster van lijst https://www.dxcluster.info/telnet/dxcluster_up.htm
PORT = 41113                    # Vervang door de poort van het DXCluster
CALLSIGN = "PA3EFR"     # Vervang door je callsign of gebruikersnaam
patterns = [r"B/", r"WWFF", r"POTA", r"COTA", r"SOTA", r"BOTA"]

def play_alarm():
    """Speelt een alarmsignaal af met vijf tonen."""
    frequencies = [1500, 1600, 1700, 1800, 1900, 2000, 2200, 2400, 2600, 2800]  # Frequenties in Hertz
    duration = 100  # Duur van elke toon in milliseconden

    for frequency in frequencies:
        winsound.Beep(frequency, duration)

def main():
    # Maak verbinding met het DXCluster
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        print(f"Verbinden met {HOST}:{PORT}...")
        s.connect((HOST, PORT))
        print("Verbonden. Verstuur login...")

        # Verstuur logincommando
        login_command = f"{CALLSIGN}\n"  # Vaak is het gewoon de callsign gevolgd door een newline
        s.sendall(login_command.encode("utf-8"))
        print(f"Ingelogd als: {CALLSIGN}")
        printnr = 0
        try:
            while True:
                # Ontvang data van de socket
                data = s.recv(1024).decode("utf-8", errors="replace")
                data = data.upper()
                if not data:
                    break  # Verbinding is gesloten door de server

                # Toon ontvangen data (optioneel)
                #print(data)


                # Controleer op "B/" in de ontvangen tekst
                for pattern in patterns:
                    if re.search(pattern, data):
                        print(f"Alarm: '{pattern}'")
                        print (data)
                        play_alarm()

                #if re.search(r"\bSOTA\b", data):
                #    print("Alarm: 'B/' gevonden!")
                #    play_alarm()

        except KeyboardInterrupt:
            print("Script gestopt door gebruiker.")
        except Exception as e:
            print(f"Fout opgetreden: {e}")

if __name__ == "__main__":
    main()

 

Maybe you want to have a warning on specific words in the comments. In that case use this flexible Cluster Warning script where the script is asking for terms. Is provided with a space, all cluster texts will get through.

import socket
import re
import winsound # Voor Windows; gebruik 'playsound' voor andere systemen.

# Configuratie: pas deze waarden aan
HOST = "www.db0erf.de"
PORT = 41113
CALLSIGN = "<callsign>"

# Vraag de gebruiker om zoekpatronen
user_input = input("Voer de zoekpatronen in, gescheiden door komma's (Spatie voor alles of Enter voor default: B/, BOTA): ")
patterns = [pattern.strip() for pattern in user_input.split(",")] if user_input else ["B/", "BOTA"]
print(f"Gebruikte zoekpatronen: {patterns}")

def play_alarm():
"""Speelt een alarmsignaal af met tonen."""
frequencies = [1500] # Frequenties in Hertz
duration = 40 # Duur van elke toon in milliseconden

for frequency in frequencies:
winsound.Beep(frequency, duration)

frequencies = [2000]
duration = 100

for frequency in frequencies:
winsound.Beep(frequency, duration)

def main():
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
print(f"Verbinden met {HOST}:{PORT}...")
s.connect((HOST, PORT))
print("Verbonden. Verstuur login...")

login_command = f"{CALLSIGN}\n"
s.sendall(login_command.encode("utf-8"))
print(f"Ingelogd als: {CALLSIGN}")

try:
while True:
data = s.recv(1024).decode("utf-8", errors="replace").upper()
if not data:
break

for pattern in patterns:
if re.search(pattern, data):
print(f"Alarm: '{pattern}'")
print(data)
play_alarm()

except KeyboardInterrupt:
print("Script gestopt door gebruiker.")
except Exception as e:
print(f"Fout opgetreden: {e}")

if __name__ == "__main__":
main()

 

 

 

Because the Thin Client uses its HRI-200 as audio control for the WIRES-X, this cluster warning system requires selecting the built-in loudspeaker. Which is known as device 15. You can have a list printed at the start of the program by removing a few #.

  • pip install playsound
  • pip install pyaudio
  • pip install numpy

import socket
import re
import winsound  # Voor Windows; gebruik 'playsound' voor andere systemen.

# Configuratie: pas deze waarden aan
HOST = "www.db0erf.de"  # Vervang door de host-IP van het DXCluster van lijst https://www.dxcluster.info/telnet/dxcluster_up.htm
PORT = 41113                    # Vervang door de poort van het DXCluster
CALLSIGN = "PA3EFR"     # Vervang door je callsign of gebruikersnaam
patterns = [r"B/", r"WWFF", r"POTA", r"COTA", r"SOTA", r"BOTA"]

import pyaudio
import numpy as np

# Initialiseer PyAudio
p = pyaudio.PyAudio()

# Lijst van apparaten
#print("Beschikbare apparaten:")                                     # Deze 4 regels activeren om een lijst van devices te zien
#for i in range(p.get_device_count()):
#    info = p.get_device_info_by_index(i)
#    print(f"{i}: {info['name']}")

# Specificeer apparaat-ID van ingebouwde luidsprekers
default_device_id = 15  # Pas dit aan op basis van bovenstaande lijst


def play_alarm():
    """Speelt een toon af als alarmgeluid."""
    fs = 44100  # Samplefrequentie
    duration = 3  # Duur van de toon in seconden
    frequency = 440.0  # Frequentie van de toon in Hertz

    # Genereer een sinusgolf
    t = np.linspace(0, duration, int(fs * duration), endpoint=False)
    tone = (np.sin(2 * np.pi * frequency * t) * 0.5).astype(np.float32)

    # Initialiseer PyAudio
    p = pyaudio.PyAudio()

    try:
        # Start een audio-stream op het opgegeven apparaat
        stream = p.open(format=pyaudio.paFloat32,
                        channels=1,
                        rate=fs,
                        output=True,
                        output_device_index= default_device_id)

        # Speel de toon af
        stream.write(tone.tobytes())
        stream.stop_stream()
        stream.close()
    finally:
        p.terminate()

def main():
    # Maak verbinding met het DXCluster
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        print(f"Verbinden met {HOST}:{PORT}...")
        s.connect((HOST, PORT))
        print("Verbonden. Verstuur login...")

        # Verstuur logincommando
        login_command = f"{CALLSIGN}\n"  # Vaak is het gewoon de callsign gevolgd door een newline
        s.sendall(login_command.encode("utf-8"))
        print(f"Ingelogd als: {CALLSIGN}")
        printnr = 0
        try:
            while True:
                # Ontvang data van de socket
                data = s.recv(1024).decode("utf-8", errors="replace")
                data = data.upper()
                if not data:
                    break  # Verbinding is gesloten door de server

                # Toon ontvangen data (optioneel)
                #print(data)


                # Controleer op pattern in de ontvangen tekst
                for pattern in patterns:
                    if re.search(pattern, data):
                        print(f"Alarm: '{pattern}'")
                        print (data)
                        play_alarm()

        except KeyboardInterrupt:
            print("Script gestopt door gebruiker.")
        except Exception as e:
            print(f"Fout opgetreden: {e}")

if __name__ == "__main__":
    main()

 

Voor het toplicht gebruiken we een Arduino. Hierbij de stappen van het instellen van deze Arduino naar een andere call.

  1. Installeer de Arduino IDE op je PC/laptop (dit is een applicatie om Arduino's te kunnen programmeren).
  2. Installeer Digispark (Default - 16.5 MHz) van https://raw.githubusercontent.com/digistump/arduino-boards-index/refs/heads/master/package_digistump_index.json (Preference verwijzing)
  3. Zet de schakelaar achterop de rotor klok in de middenstand en sluit alleen de USB-A to USB-A kabel aan de rotorklok aan. Steek deze nog niet in de laptop.
  4. Start Arduino en pas de Sketch.ino (zie hieronder) aan naar de nieuwe call (regel 134, bold highlight)
  5. Druk linksboven in de IDE op het pijltje naar rechts (upload) en wacht tot onderin het scherm de melding komt om "binnen 60 seconden het device aan te sluiten". Druk binnen deze 60 seconden de USB-A to USB-A kabel in de laptop.
  6. Na 4 seconden is de INO-file geupload en klaar voor gebruik. USB-A to USB-A kabel mag nu verwijderd worden.
  7. Check met de schakelaar naar beneden of de call juist wordt geseind. 

//
// Simple Arduino Morse Beacon
// Written by Mark VandeWettering K6HX
// Email: This email address is being protected from spambots. You need JavaScript enabled to view it.
// 
// This code is so trivial that I'm releasing it completely without 
// restrictions.  If you find it useful, it would be nice if you dropped
// me an email, maybe plugged my blog @ https://brainwagon.org or included
// a brief acknowledgement in whatever derivative you create, but that's
// just a courtesy.  Feel free to do whatever.
//


struct t_mtab { char c, pat; } ;

struct t_mtab morsetab[] = {
    {'.', 106},
  {',', 115},
  {'?', 76},
  {'/', 41},
  {'A', 6},
  {'B', 17},
  {'C', 21},
  {'D', 9},
  {'E', 2},
  {'F', 20},
  {'G', 11},
  {'H', 16},
  {'I', 4},
  {'J', 30},
  {'K', 13},
  {'L', 18},
  {'M', 7},
  {'N', 5},
  {'O', 15},
  {'P', 22},
  {'Q', 27},
  {'R', 10},
  {'S', 8},
  {'T', 3},
  {'U', 12},
  {'V', 24},
  {'W', 14},
  {'X', 25},
  {'Y', 29},
  {'Z', 19},
  {'1', 62},
  {'2', 60},
  {'3', 56},
  {'4', 48},
  {'5', 32},
  {'6', 33},
  {'7', 35},
  {'8', 39},
  {'9', 47},
  {'0', 63}
} ;

#define N_MORSE  (sizeof(morsetab)/sizeof(morsetab[0]))

#define SPEED  (8)
#define DOTLEN  (1200/SPEED)
#define DASHLEN  (3*(1200/SPEED))

const int LEDpin = 3;    // the pin that the LED is attached to


void
dash()
{
  digitalWrite(LEDpin, HIGH) ;
  delay(DASHLEN);
  digitalWrite(LEDpin, LOW) ;
  delay(DOTLEN) ;
}

void
dit()
{
  digitalWrite(LEDpin, HIGH) ;
  delay(DOTLEN);
  digitalWrite(LEDpin, LOW) ;
  delay(DOTLEN);
}

void
send(char c)
{
  int i ;
  if (c == ' ') {
    Serial.print(c) ;
    delay(7*DOTLEN) ;
    return ;
  }
  for (i=0; i<N_MORSE; i++) {
    if (morsetab[i].c == c) {
      unsigned char p = morsetab[i].pat ;
      Serial.print(morsetab[i].c) ;

      while (p != 1) {
          if (p & 1)
            dash() ;
          else
            dit() ;
          p = p / 2 ;
      }
      delay(4*DOTLEN) ;
      return ;
    }
  }
  /* if we drop off the end, then we send a space */
  Serial.print("?") ;
}

void
sendmsg(char *str)
{
  while (*str)
    send(*str++) ;
  Serial.println("");
}

void setup() {
  pinMode(LEDpin, OUTPUT) ;
  digitalWrite(LEDpin, LOW) ;
  Serial.begin(9600) ;
  Serial.println("Simple Arduino Morse Beacon v0.0") ;
  Serial.println("Written by Mark VandeWettering <This email address is being protected from spambots. You need JavaScript enabled to view it.>") ;
  Serial.println("Check out my blog @ https://brainwagon.org") ;
  Serial.println("") ;
}

void loop() {
  sendmsg("PA3EFR/J") ;
  delay(3000) ;
}

 

 

Voor het vergelijken van een ADIF file (format: *.adi) en een Excel file (format: *.xslx) heb ik een script gemaakt om vast te stellen of een callsign en een bijbehorende datum in beide files voorkomt. Het resultaat wordt in de terminal weergegeven en als output file (format: *.xlsx) weggeschreven. Alle files moeten in dezelfde directory worden samengevoegd.

Indien nodig: pip install tabulate

import pandas as pd
import re
from tabulate import tabulate
from datetime import datetime

def read_adif(file_path):
"""Leest een ADIF-bestand en extraheert relevante velden."""
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()

records = []
qsos = content.strip().split('<eor>')
for qso in qsos:
call = re.search(r'<call:(\d+)>([^<]*)', qso)
date = re.search(r'<qso_date:(\d+)>([^<]*)', qso)
time = re.search(r'<time_on:(\d+)>([^<]*)', qso)
band = re.search(r'<band:(\d+)>([^<]*)', qso)
mode = re.search(r'<mode:(\d+)>([^<]*)', qso)

if call:
# Verkrijg de pure callsign zonder prefix of suffix
raw_call = call.group(2).strip()

# Verwijder prefix of suffix (maximaal 2 tekens voor en na de '/')
clean_call = re.sub(r'^[a-zA-Z0-9]{1,2}/|/([a-zA-Z0-9]{1,2})$', '', raw_call)

# Converteer de datum naar het juiste formaat "YYYYMMDD"
qso_date = date.group(2).strip() if date else ''
if qso_date:
try:
# Als de datum in "YYYY-MM-DD HH:MM:SS" formaat is, zet het dan om naar "YYYYMMDD"
qso_date_obj = datetime.strptime(qso_date, "%Y-%m-%d %H:%M:%S")
qso_date = qso_date_obj.strftime("%Y%m%d") # Zet het om naar "YYYYMMDD"
except ValueError:
pass # Als de conversie mislukt, blijf de originele datum behouden

records.append({
'call': clean_call,
'qso_date': qso_date,
'time_on': time.group(2).strip() if time else '',
'band': band.group(2).strip() if band else '',
'mode': mode.group(2).strip() if mode else ''
})

return pd.DataFrame(records)

def read_xlsx(file_path, sheet_name='Hunter'):
"""Leest een Excel-bestand en haalt relevante kolommen op zonder kolomnamen."""
df = pd.read_excel(file_path, sheet_name=sheet_name, dtype=str, header=None)
df = df.iloc[:, [2, 4, 5, 6, 7]] # Kolommen C, E, F, G, H
df.columns = ['call', 'qso_date', 'time_on', 'band', 'mode']

# Zet de date om van "YYYY-MM-DD HH:MM:SS" naar "YYYYMMDD"
df['qso_date'] = pd.to_datetime(df['qso_date'], format='%Y-%m-%d %H:%M:%S', errors='coerce').dt.strftime('%Y%m%d')

# Verwijder prefix en suffix van de call (maximaal 2 tekens voor en na de '/')
df['call'] = df['call'].apply(
lambda x: re.sub(r'^[a-zA-Z0-9]{1,2}/|/([a-zA-Z0-9]{1,2})$', '', str(x)) if isinstance(x, str) else '')

return df.dropna()

def compare_files(adif_file, xlsx_file, output_file):
"""Vergelijkt de ADIF- en XLSX-bestanden en slaat de overeenkomsten op."""
df_adif = read_adif(adif_file)
df_xlsx = read_xlsx(xlsx_file)

# Merge op basis van zowel 'call' als 'qso_date'
matched_df = df_adif.merge(df_xlsx, on=['call', 'qso_date'], how='inner')


# Tel het aantal records in het matched DataFrame
num_records = len(matched_df)
print(f"\nAantal overeenkomsten gevonden: {num_records}")

# Druk de matched DataFrame af in tabelvorm met tabulate
print("\nGecombineerde gegevens (tabelvorm):")
print(tabulate(matched_df, headers='keys', tablefmt='pretty', showindex=False))
print(f"\nAantal overeenkomsten gevonden: {num_records}")
# Opslaan naar een nieuwe Excel-file
matched_df.to_excel(output_file, index=False)
print(f"\nResultaten opgeslagen in {output_file}")

# Bestanden invoeren
adif_file = input("Voer de naam van het outputbestand in [default=ADIF.adi]: ") or "ADIF.adi"
xlsx_file = input("Voer de naam van het outputbestand in [default=WWBOTA.xlsx]: ") or "WWBOTA.xlsx"
output_file = input("Voer de naam van het outputbestand in [default=OUTPUT.xlsx]: ") or "OUTPUT.xlsx"


compare_files(adif_file, xlsx_file, output_file)

 

Voor het POTA award van PA heb ik een tweetal scripts geschreven: een sorteer script om het laatste volgnummer te vinden en een toewijzing van tekst aan een JPG.

import pandas as pd

# Bestandspad
file_name = "AwardGrantsOverview.xlsx"

# Inlezen van de Excel-bestand
data = pd.read_excel(file_name)

# Controleren of de benodigde kolommen bestaan
required_columns = ['Color', 'Type', 'Number']
if not all(col in data.columns for col in required_columns):
raise ValueError(f"De Excel file moet de volgende kolommen bevatten: {', '.join(required_columns)}")

# Sorteren op Color, Type en Number
data_sorted = data.sort_values(by=['Color', 'Type', 'Number'], ascending=[True, True, True])

# Groeperen op Color en Type en het hoogste nummer vinden
grouped = data_sorted.groupby(['Color', 'Type'])
highest_numbers = grouped['Number'].max()

# Printen van de resultaten
print("Hoogste nummers voor elke Color-Type combinatie:\n")
for (color, type_), number in highest_numbers.items():
print(f"\t{color}, {type_}, Hoogste Number: {number}")

# Optioneel: opslaan van de gesorteerde data in een nieuw Excel-bestand
data_sorted.to_excel("AwardGrantsOverview.xlsx", index=False)
print("\n\n> Gesorteerde data is opgeslagen in 'AwardGrantsOverview.xlsx'.")
from PIL import Image, ImageDraw, ImageFont
import os
from datetime import datetime
from openpyxl import load_workbook

import Sort

# Function to validate input with repeated prompts
def get_valid_input(prompt, valid_options):
while True:
value = input(prompt).strip().lower()
if value in valid_options:
return value
print(f"Invalid input. Choose from {', '.join(valid_options)}.")


# Function to draw text with a border (outline)
def draw_text_with_border(draw, text, position, font, text_color, border_color, border_thickness):
# Draw border by rendering text multiple times around the original position
x, y = position
for offset_x in range(-border_thickness, border_thickness + 1):
for offset_y in range(-border_thickness, border_thickness + 1):
# Skip the center position (to avoid drawing the text multiple times in the exact same spot)
if offset_x == 0 and offset_y == 0:
continue
# Draw the border (outline)
draw.text((x + offset_x, y + offset_y), text, font=font, fill=border_color)

# Draw the main text over the border (in the desired text color)
draw.text(position, text, font=font, fill=text_color)


# Ask for input details
color_input = get_valid_input("\nEnter the color (options: b for Bronze, s for Silver, g for Gold): ", ["b", "s", "g"])

# Map the color input to the full color name
if color_input == "b":
color = "bronze"
elif color_input == "s":
color = "silver"
elif color_input == "g":
color = "gold"

# Ask for the activator/hunter option
type_option = get_valid_input("\n\t Is it an activator (a) or a hunter (h)? (options: a, h): ", ["a", "h"])

# Map single-letter input to full words
type_option = "activator" if type_option == "a" else "hunter"

name = input("\n\t Enter the name to be added (e.g., 'Erwin - PA3EFR'): ").strip()
serial_number = input("\n\t\t\t\t\t Enter the serial number: ").strip()

# Get today's date
today_date = datetime.now().strftime("%Y-%m-%d")

# Construct the correct input file path based on color and activator/hunter
input_directory = color.capitalize() # Directory name: Bronze, Silver, Gold
input_image_name = f"{color.capitalize()}{type_option.capitalize()}.jpg"
input_image_path = os.path.join(input_directory, input_image_name)

# Check if the file exists
if not os.path.exists(input_image_path):
print(f"The file '{input_image_path}' does not exist. Please check the name and try again.")
exit()

# Create the output file name
sanitized_name = name.replace(" ", "-").replace("/", "-")
output_pdf_name = f"{serial_number}_{color}_{type_option}_{sanitized_name}.pdf"
output_pdf_path = os.path.join(input_directory, output_pdf_name)

try:
# Open the image
img = Image.open(input_image_path)

# Create a drawing object
draw = ImageDraw.Draw(img)

# Choose a font and size (adjust the path if necessary)
font_size_name = int(150) # Fixed font size
font_size_number = int(100) # Fixed font size
font_size_date = int(80) # Fixed font size

try:
font_name = ImageFont.truetype("Bodoni Bd BT Bold.ttf", font_size_name)
font_number = ImageFont.truetype("arial.ttf", font_size_number)
font_date = ImageFont.truetype("arial.ttf", font_size_date)
except IOError:
# Fallback to a default font if arial.ttf is not available
font_name = ImageFont.load_default()
font_number = ImageFont.load_default()
font_date = ImageFont.load_default()

# Calculate the position of the name
text_name_bbox = draw.textbbox((0, 0), name, font=font_name) # (left, top, right, bottom)
text_name_width = text_name_bbox[2] - text_name_bbox[0]
name_position = (
200, # pix from left
650 # pix from the top (140 pixels at 300 DPI)
)

# Calculate the position of the serial number
text_number_bbox = draw.textbbox((0, 0), serial_number, font=font_number) # (left, top, right, bottom)
text_number_width = text_number_bbox[2] - text_number_bbox[0]
number_position = (
3200, # pixels from the left edge
505 # pix from the top (140 pixels at 300 DPI)
)

# Calculate the position of the date
text_date_bbox = draw.textbbox((0, 0), today_date, font=font_date) # (left, top, right, bottom)
date_position = (
3005, # pixels from the left edge
396 # pix from the top (140 pixels at 300 DPI)
)

# Add the name with a white border and black text
draw_text_with_border(draw, name, name_position, font_name, (0, 0, 0), (255, 255, 255), 5) # Border thickness = 5

# Add the serial number with a white border and black text
draw_text_with_border(draw, serial_number, number_position, font_number, (0, 0, 0), (255, 255, 255), 5)

# Add the date with a white border and red text
draw_text_with_border(draw, today_date, date_position, font_date, (0, 0, 0), (255, 255, 255), 5)

# Convert the image to RGB (necessary for PDF export)
if img.mode in ("RGBA", "P"): # Check if conversion is needed
img = img.convert("RGB")

# Save the image as a PDF in the correct directory
img.save(output_pdf_path, "PDF")
print(f"\n\n> The image with added text has been saved as '{output_pdf_path}'.")

# Now add the information to the Excel file
try:
# Load the existing workbook
workbook = load_workbook("AwardGrantsOverview.xlsx")
sheet = workbook.active

# Add a new row with the data
sheet.append([color.capitalize(), type_option.capitalize(), name, serial_number, output_pdf_name, today_date])

# Save the changes to the Excel file
workbook.save("AwardGrantsOverview.xlsx")
print("> The data has been added to 'AwardGrantsOverview.xlsx'.")

except Exception as e:
print(f"An error occurred while updating the Excel file: {e}")

except Exception as e:
print(f"An error occurred: {e}")