Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ Usage of ./termlog:
| Ctrl+R | Force screen redraw |
| Alt+Left | Tune down 500khz |
| Alt+Right | Tune up 500khz |
| Ctrl+C | Show band conditions |

## adif

Expand Down
11 changes: 11 additions & 0 deletions cmd/termlog/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/tzneal/ham-go/adif"
"github.com/tzneal/ham-go/callsigns"
"github.com/tzneal/ham-go/cmd/termlog/ui"
"github.com/tzneal/ham-go/solar"
"github.com/tzneal/ham-go/spotting"
)

Expand Down Expand Up @@ -79,6 +80,12 @@ type SOTASpot struct {
URL string
}

// HamQSL allows solar condition monitoring
type HamQSL struct {
Enabled bool
URL string
}

// Label is a label that will be displayed when tuned to a particular frequency.
// The start/end are the limits.
type Label struct {
Expand All @@ -99,6 +106,7 @@ type Config struct {
DXCluster DXCluster
POTASpot POTASpot
SOTASpot SOTASpot
HamQSL HamQSL
Theme ui.Theme
Label []Label
noNet bool // lowercase, so it shouldn't be serialized
Expand Down Expand Up @@ -209,6 +217,9 @@ func NewConfig() *Config {
cfg.SOTASpot.Enabled = true
cfg.SOTASpot.URL = spotting.SOTAURL

cfg.HamQSL.Enabled = true
cfg.HamQSL.URL = solar.HAMQSL_URL

// 160 meters
cfg.Label = append(cfg.Label, Label{
Name: "E/A/G/Data/Phone",
Expand Down
63 changes: 59 additions & 4 deletions cmd/termlog/mainscreen.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/tzneal/ham-go/logingest"
"github.com/tzneal/ham-go/logsync"
"github.com/tzneal/ham-go/rig"
"github.com/tzneal/ham-go/solar"
"github.com/tzneal/ham-go/spotting"
)

Expand All @@ -49,6 +50,7 @@ type mainScreen struct {
shutdown chan struct{}
lookup callsigns.Lookup
loggingReplaced bool
solar *solar.Solar
}
type logRequest struct {
record adif.Record
Expand Down Expand Up @@ -109,7 +111,8 @@ func newMainScreen(cfg *Config, alog *adif.Log, repo *git.Repository, bookmarks

// is the spot monitoring enabled?
shutdown := make(chan struct{})
if !cfg.noNet && (cfg.DXCluster.Enabled || cfg.POTASpot.Enabled) {
if !cfg.noNet && (cfg.DXCluster.Enabled || cfg.POTASpot.Enabled ||
cfg.SOTASpot.Enabled) {
// create the UI
dxHeight := remainingHeight - 1 - msgHeight // -1 due to status bar
spotlist := ui.NewSpottingList(yPos, dxHeight, time.Duration(cfg.Operator.SpotExpiration)*time.Second, cfg.Theme)
Expand Down Expand Up @@ -330,6 +333,7 @@ func newMainScreen(cfg *Config, alog *adif.Log, repo *git.Repository, bookmarks
ms.js8log.Run()
}
}

if cfg.FLLog.Enabled {
fldigiLog, err := logingest.NewFLDIGIServer(cfg.FLLog.Address)
if err != nil {
Expand All @@ -341,6 +345,25 @@ func newMainScreen(cfg *Config, alog *adif.Log, repo *git.Repository, bookmarks
}
}

if !cfg.noNet && cfg.HamQSL.Enabled {
hcfg := solar.HamQSLConfig{
URL: cfg.HamQSL.URL,
}
hclient := solar.NewHamQSLClient(hcfg)
hclient.Run()
go func() {
for {
select {
case <-shutdown:
return
case s := <-hclient.Solar:
ms.solar = &s
_ = s
}
}
}()
}

c.AddCommand(input.KeyCtrlH, ms.showHelp)
c.AddCommand(input.KeyCtrlL, ms.focusQSOList)
c.AddCommand(input.KeyCtrlN, ms.newQSO)
Expand All @@ -351,6 +374,7 @@ func newMainScreen(cfg *Config, alog *adif.Log, repo *git.Repository, bookmarks
c.AddCommand(input.KeyCtrlG, ms.commitLog)
c.AddCommand(input.KeyCtrlR, ms.redrawAll)
c.AddCommand(input.KeyCtrlX, ms.exportCabrillo)
c.AddCommand(input.KeyCtrlC, ms.showConditions)

c.AddCommand(input.KeyCtrlE, ms.executeCommands)
c.AddCommand(input.KeyAltLeft, ms.tuneLeft)
Expand Down Expand Up @@ -704,6 +728,7 @@ func (m *mainScreen) newQSO() {
func (m *mainScreen) focusQSOList() {
m.controller.Focus(m.qsoList)
}

func (m *mainScreen) saveQSO() {
if m.qso.IsValid() || ui.YesNoQuestion("Missing callsign or frequency, save anyway?") {
rec := m.qso.GetRecord()
Expand All @@ -720,6 +745,36 @@ func (m *mainScreen) saveQSO() {
}
}

func (m *mainScreen) showConditions() {
sb := strings.Builder{}

formatDate := m.solar.SolarData.Updated.Format("01-02-2006 15:04:05")
sb.WriteString(fmt.Sprintf("%s UTC\n", formatDate))
sb.WriteString("\n")
sb.WriteString(fmt.Sprintf("Sunspots: %d\n", m.solar.SolarData.Sunspots))
sb.WriteString(fmt.Sprintf("Solar Flux: %d\n", m.solar.SolarData.SolarFlux))
sb.WriteString(fmt.Sprintf("MUF: %0.2f MHz\n", m.solar.SolarData.MUF))
sb.WriteString(fmt.Sprintf("K Index: %d\n", m.solar.SolarData.KIndex))
sb.WriteString(fmt.Sprintf("A Index: %d\n", m.solar.SolarData.AIndex))
sb.WriteString(fmt.Sprintf("Signal Noise: %s\n", m.solar.SolarData.SignalNoise))
sb.WriteString("\n")
for _, band := range m.solar.SolarData.CalculatedConditions.Bands {
// Add spaces to day to make as long as night
timeStr := band.Time
spacesToAdd := 5 - len(timeStr)
for i := 0; i < spacesToAdd; i++ {
timeStr += " "
}
sb.WriteString(fmt.Sprintf("%s %s\t%s", band.Name, timeStr, band.Condition))
sb.WriteString("\n")
}

sb.WriteString("\n")
sb.WriteString("Press ESC to close")
ui.Splash("Conditions", sb.String())

}

func (m *mainScreen) showHelp() {
sb := strings.Builder{}
sb.WriteString("Ctrl+H - Show Help Ctrl+Q - Quit\n")
Expand All @@ -739,12 +794,12 @@ func (m *mainScreen) showHelp() {
sb.WriteString("Ctrl+E - Display Custom Commands\n")
sb.WriteString("Ctrl+G - Commit log file to git\n")
sb.WriteString("Ctrl+R - Force Screen Redraw\n")
sb.WriteString("ALt+Left - Tune Down\n")
sb.WriteString("ALt+Right - Tune Up\n")
sb.WriteString("Alt+Left - Tune Down\n")
sb.WriteString("Alt+Right - Tune Up\n")
sb.WriteString("Ctrl+C - Show Band Conditions\n")
sb.WriteString("\n")
sb.WriteString("Press ESC to close")
ui.Splash("Commands", sb.String())

}

func (m *mainScreen) Tick() bool {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/nsf/termbox-go v0.0.0-20200418040025-38ba6e5628f1
github.com/pd0mz/go-maidenhead v0.0.0-20170221185439-faa09c24425e
github.com/stretchr/testify v1.7.0 // indirect
go.etcd.io/bbolt v1.3.4
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 // indirect
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,19 @@ github.com/pd0mz/go-maidenhead v0.0.0-20170221185439-faa09c24425e h1:Wi4WahFJ11u
github.com/pd0mz/go-maidenhead v0.0.0-20170221185439-faa09c24425e/go.mod h1:4Q+QSDCqWqlabstLGUVm47rAcL06nEEty2d3KzsTNMk=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/term v0.0.0-20180423043932-cda20d4ac917/go.mod h1:eCbImbZ95eXtAUIbLAuAVnBnwf83mjf6QIVH8SHYwqQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/src-d/gcfg v1.3.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/wunderlist/ttlcache v0.0.0-20140611003616-fa8f18d5e019/go.mod h1:oWWm4B/FRe5AKcl+/5tz6YaA4HWpzzt5hSKM5+LSYgM=
github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70=
github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
Expand Down Expand Up @@ -95,3 +101,5 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
148 changes: 148 additions & 0 deletions solar/hamqsl_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package solar

import (
"encoding/xml"
"log"
"net/http"
"strings"
"time"
)

const HAMQSL_URL = "http://www.hamqsl.com/solarxml.php"

type HamQSLConfig struct {
URL string // HamQSL API url, defaults to http://www.hamqsl.com/solarxml.php
Delay time.Duration // Delay between SPOT checks, defaults to 60 minutes with a minimum of 60 minutes
}

// HamQSL is a client for retrieving solar forecast's
// relevant to propagation
type HamQSLClient struct {
Solar chan Solar

config HamQSLConfig
shutdown chan struct{}
}

type Band struct {
Name string `xml:"name,attr"` // Band range (e.g. 80m-40m)
Time string `xml:"time,attr"` // Day or night
Condition string `xml:",chardata"` // Condition (e.g. Good, Fair)
}

type Phenomenon struct {
Name string `xml:"name,attr"` // (e.g. E-Skip)
Location string `xml:"location,attr"` // (e.g. north_america)
Condition string `xml:",chardata"` // (e.g. Band Closed)
}

type CalculatedConditions struct {
Bands []Band `xml:"band"`
}

type CalculatedConditionsVHF struct {
Phenomenon []Phenomenon `xml:"phenomenon"`
}

type SolarData struct {
UpdatedStr string `xml:"updated"`
Updated time.Time
SolarFlux int `xml:"solarflux"`
AIndex int `xml:"aindex"`
KIndex int `xml:"kindex"`
KIndexNt string `xml:"kindexnt"`
XRay string `xml:"xray"`
Sunspots int `xml:"sunspots"`
HeliumLine string `xml:"heliumline"`
ProtonFlux string `xml:"protonflux"`
ElectronFlux string `xml:"electronflux"`
Aurora int `xml:"aurora"`
Normalization float32 `xml:"normalization"`
LatDegree float32 `xml:"latdegree"`
SolarWind float32 `xml:"solarwind"`
MagneticField float32 `xml:"magneticfield"`
GeomagneticField string `xml:"geomagfield"`
SignalNoise string `xml:"signalnoise"`
Fof2 float32 `xml:"fof2"`
MUFFactor float32 `xml:"muffactor"`
MUF float32 `xml:"muf"`
CalculatedConditions CalculatedConditions `xml:"calculatedconditions"`
CalculatedConditionsVHF CalculatedConditionsVHF `xml:"calculatedvhfconditions"`
}

type Solar struct {
SolarData SolarData `xml:"solardata"`
}

// NewHamQSLClient constructs a new HamQSL client
func NewHamQSLClient(cfg HamQSLConfig) *HamQSLClient {
// enforce a minimum poll delay of once per hour
if cfg.Delay < 60*time.Minute {
cfg.Delay = 60 * time.Minute
}
if cfg.URL == "" {
cfg.URL = HAMQSL_URL
}
client := &HamQSLClient{
config: cfg,
Solar: make(chan Solar),
shutdown: make(chan struct{}),
}
return client
}

// Close gracefully shuts down the client
func (c *HamQSLClient) Close() error {
close(c.shutdown)
return nil
}

// Run is a non-blocking call that starts the client
func (c *HamQSLClient) Run() {
go c.run()
}

func (c *HamQSLClient) run() {
poll := func() bool {
req, err := http.NewRequest("GET", c.config.URL, nil)
if err != nil {
log.Printf("error forming HamQSL request: %s", err)
return false
}
req.Header.Set("User-Agent", "ham-go")
rsp, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("error fetching HamQSL solar data: %s", err)
return true
}
dec := xml.NewDecoder(rsp.Body)
defer rsp.Body.Close()
var result Solar
if err := dec.Decode(&result); err != nil {
log.Printf("error parsing HamQSL solar data: %s", err)
}
updatedStr := strings.Trim(result.SolarData.UpdatedStr, " ")
updatedTime, err := time.Parse("02 Jan 2006 1504 MST", updatedStr)
if err != nil {
log.Printf("error parsing updated time '%s': %s", updatedStr, err)
} else {
result.SolarData.Updated = updatedTime
c.Solar <- result
}
return true
}

// do an initial poll
poll()
for {
select {
case <-time.After(c.config.Delay):
if !poll() {
return
}
case <-c.shutdown:
close(c.Solar)
return
}
}
}
Loading