mirror of
https://github.com/la5nta/pat
synced 2026-08-13 17:46:54 -04:00
165 lines
5.5 KiB
Go
165 lines
5.5 KiB
Go
package voacap
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"time"
|
|
|
|
"github.com/pd0mz/go-maidenhead"
|
|
)
|
|
|
|
// EncodeParams holds the input parameters for a VOACAP prediction.
|
|
type EncodeParams struct {
|
|
DateTime time.Time
|
|
From string
|
|
To string
|
|
SSN int
|
|
TransmitPower int
|
|
MinSNR int
|
|
Frequency float64 // Frequency in MHz
|
|
MinTakeoffAngle float64 // Minimum takeoff angle in degrees
|
|
MultipathDelay float64 // Multipath delay in ms
|
|
MultipathTolerance float64 // Multipath tolerance in dB
|
|
LocalNoise float64 // Local noise at 3 MHz in -dBw/Hz
|
|
IncludeMUF bool // Whether to include MUF in prediction output
|
|
}
|
|
|
|
// Encode writes the input data for a VOACAP prediction to the given writer.
|
|
func Encode(w io.Writer, params EncodeParams) error {
|
|
from, err := maidenhead.ParseLocator(params.From)
|
|
if err != nil {
|
|
return fmt.Errorf("error converting 'from' Maidenhead locator: %w", err)
|
|
}
|
|
to, err := maidenhead.ParseLocator(params.To)
|
|
if err != nil {
|
|
return fmt.Errorf("error converting 'to' Maidenhead locator: %w", err)
|
|
}
|
|
|
|
// VOACAP expects time in UTC
|
|
params.DateTime = params.DateTime.UTC()
|
|
|
|
writeCommentCard(w, fmt.Sprintf("%.1f MHz, %s -> %s @ %s", params.Frequency, params.From, params.To, params.DateTime.Truncate(time.Second).Format(time.RFC3339)))
|
|
writeLineMaxCard(w, 55)
|
|
writeCoeffsCard(w, "CCIR")
|
|
writeTimeCard(w, params.DateTime.Hour(), params.DateTime.Hour(), 1, 1)
|
|
writeMonthCard(w, params.DateTime.Year(), params.DateTime.Month())
|
|
writeSunspotCard(w, params.SSN)
|
|
writeLabelCard(w, params.From, params.To)
|
|
writeCircuitCard(w, from.Latitude, from.Longitude, to.Latitude, to.Longitude, 0)
|
|
// Default values from express-params.md if not specified
|
|
minTakeoffAngle := params.MinTakeoffAngle
|
|
if minTakeoffAngle == 0 {
|
|
minTakeoffAngle = 15.0
|
|
}
|
|
multipathDelay := params.MultipathDelay
|
|
if multipathDelay == 0 {
|
|
multipathDelay = 0.1
|
|
}
|
|
multipathTolerance := params.MultipathTolerance
|
|
if multipathTolerance == 0 {
|
|
multipathTolerance = 0.5
|
|
}
|
|
localNoise := params.LocalNoise
|
|
if localNoise == 0 {
|
|
localNoise = 145.0
|
|
}
|
|
writeSystemCard(w, float64(params.MinSNR), minTakeoffAngle, multipathDelay, multipathTolerance, localNoise)
|
|
writeFprobCard(w)
|
|
writeAntennaCard(w, 1, 1, 2, 30, params.Frequency, "", 90.0, float64(params.TransmitPower))
|
|
writeAntennaCard(w, 2, 2, 2, 30, params.Frequency, "", 270.0, float64(params.TransmitPower))
|
|
writeFrequencyCard(w, params.Frequency)
|
|
writeMethodCard(w, 30, 0)
|
|
writeExecuteCard(w)
|
|
writeQuitCard(w)
|
|
return nil
|
|
}
|
|
|
|
func getLatHemi(lat float64) string {
|
|
if lat < 0 {
|
|
return "S"
|
|
}
|
|
return "N"
|
|
}
|
|
|
|
func getLonHemi(lon float64) string {
|
|
if lon < 0 {
|
|
return "W"
|
|
}
|
|
return "E"
|
|
}
|
|
|
|
func writeCommentCard(w io.Writer, comment string) {
|
|
fmt.Fprintf(w, "COMMENT %-74s\n", comment)
|
|
}
|
|
|
|
func writeLineMaxCard(w io.Writer, maxLines int) {
|
|
fmt.Fprintf(w, "LINEMAX %5d\n", maxLines)
|
|
}
|
|
|
|
func writeCoeffsCard(w io.Writer, coeffs string) {
|
|
fmt.Fprintf(w, "COEFFS %-4s\n", coeffs)
|
|
}
|
|
|
|
func writeTimeCard(w io.Writer, start, end, step, mode int) {
|
|
start, end = cmp.Or(start, 24), cmp.Or(end, 24) // Midnight hour is represented as 24
|
|
fmt.Fprintf(w, "TIME %5d%5d%5d%5d\n", start, end, step, mode)
|
|
}
|
|
|
|
func writeMonthCard(w io.Writer, year int, month time.Month) {
|
|
fmt.Fprintf(w, "MONTH %5d%5.2f\n", year, float64(month))
|
|
}
|
|
|
|
func writeSunspotCard(w io.Writer, sunspotNumber int) {
|
|
fmt.Fprintf(w, "SUNSPOT %5.2f\n", float64(sunspotNumber))
|
|
}
|
|
|
|
func writeLabelCard(w io.Writer, txName, rxName string) {
|
|
fmt.Fprintf(w, "LABEL %-20s%-20s\n", txName, rxName)
|
|
}
|
|
|
|
func writeCircuitCard(w io.Writer, txLat, txLon, rxLat, rxLon float64, path int) {
|
|
fmt.Fprintf(w, "CIRCUIT %5.2f%s%9.2f%s%9.2f%s%9.2f%s S %5d\n",
|
|
math.Abs(txLat), getLatHemi(txLat),
|
|
math.Abs(txLon), getLonHemi(txLon),
|
|
math.Abs(rxLat), getLatHemi(rxLat),
|
|
math.Abs(rxLon), getLonHemi(rxLon),
|
|
path)
|
|
}
|
|
|
|
func writeSystemCard(w io.Writer, requiredSnr, minTakeoffAngle, multipathDelay, multipathTolerance, localNoise float64) {
|
|
// Parameter order according to VOACAP documentation:
|
|
// fixed-factor, local-noise, multipath-delay, min-takeoff-angle, required-SNR, power-tolerance, delay-tolerance
|
|
//
|
|
// NOTE: There appears to be a mismatch in VOACAP where it shows "Minimum Angle= 0.100 degrees" in the output
|
|
// despite us setting minTakeoffAngle to 15.0. This suggests that VOACAP is incorrectly using the
|
|
// multipathDelay value (0.10) as the minimum angle in its output header. However, the actual
|
|
// calculations seem to be using the correct takeoff angle value, as we can see in the TANGLE output rows.
|
|
fmt.Fprintf(w, "SYSTEM %5.2f%5.0f%5.2f%5.0f%5.2f%5.2f%5.2f\n", 1.0, localNoise, multipathDelay, minTakeoffAngle, requiredSnr, multipathTolerance, multipathDelay)
|
|
}
|
|
|
|
func writeFprobCard(w io.Writer) {
|
|
fmt.Fprintf(w, "FPROB %5.2f%5.2f%5.2f%5.2f\n", 1.0, 1.0, 1.0, 0.0)
|
|
}
|
|
|
|
func writeAntennaCard(w io.Writer, antType, id, minFreq, maxFreq int, freqMHz float64, antFile string, bearing, powerW float64) {
|
|
powerKW := powerW / 1000.0
|
|
fmt.Fprintf(w, "ANTENNA %5d%5d%5d%5d%10.3f[%-21s]%5.1f%10.4f\n", antType, id, minFreq, maxFreq, freqMHz, antFile, bearing, powerKW)
|
|
}
|
|
|
|
func writeFrequencyCard(w io.Writer, freqMHz float64) {
|
|
fmt.Fprintf(w, "FREQUENCY %5.3f\n", freqMHz)
|
|
}
|
|
|
|
func writeMethodCard(w io.Writer, method, spec int) {
|
|
fmt.Fprintf(w, "METHOD %5d%5d\n", method, spec)
|
|
}
|
|
|
|
func writeExecuteCard(w io.Writer) {
|
|
fmt.Fprintln(w, "EXECUTE")
|
|
}
|
|
|
|
func writeQuitCard(w io.Writer) {
|
|
fmt.Fprintln(w, "QUIT")
|
|
}
|