diff --git a/core/app/app.go b/core/app/app.go
index 065fe0b..d7508bd 100644
--- a/core/app/app.go
+++ b/core/app/app.go
@@ -22,6 +22,7 @@ import (
"github.com/ftl/hellocontest/core/export/adif"
"github.com/ftl/hellocontest/core/export/cabrillo"
"github.com/ftl/hellocontest/core/export/csv"
+ "github.com/ftl/hellocontest/core/hamdxmap"
"github.com/ftl/hellocontest/core/keyer"
"github.com/ftl/hellocontest/core/logbook"
"github.com/ftl/hellocontest/core/newcontest"
@@ -66,6 +67,7 @@ type Controller struct {
dxccFinder *dxcc.Finder
scpFinder *scp.Finder
callHistoryFinder *callhistory.Finder
+ hamDXMap *hamdxmap.HamDXMap
VFO *vfo.VFO
Logbook *logbook.Logbook
@@ -97,6 +99,7 @@ type View interface {
// Configuration provides read access to the configuration data.
type Configuration interface {
LogDirectory() string
+ HamDXMapPort() int
Station() core.Station
Contest() core.Contest
KeyerSettings() core.KeyerSettings
@@ -144,6 +147,7 @@ func (c *Controller) Startup() {
c.dxccFinder = dxcc.New()
c.scpFinder = scp.New()
c.callHistoryFinder = callhistory.New(c.Settings, c.ServiceStatus.StatusChanged)
+ c.hamDXMap = hamdxmap.NewHamDXMap(c.configuration.HamDXMapPort())
c.Score = score.NewCounter(c.Settings, c.dxccFinder)
c.QSOList = logbook.NewQSOList(c.Settings, c.Score)
@@ -156,7 +160,7 @@ func (c *Controller) Startup() {
c.Bandmap,
c.asyncRunner,
)
- c.Entry.Notify(c.Bandmap)
+ c.Entry.Notify(c.hamDXMap)
c.Bandmap.Notify(c.Entry)
c.QSOList.Notify(c.Entry)
c.Score.Notify(c.Bandmap)
@@ -233,6 +237,10 @@ func (c *Controller) Startup() {
c.scpFinder.WhenAvailable(func() {
c.ServiceStatus.StatusChanged(core.SCPService, true)
})
+ c.ServiceStatus.StatusChanged(core.MapService, true)
+ c.hamDXMap.WhenStopped(func() {
+ c.ServiceStatus.StatusChanged(core.MapService, false)
+ })
c.Entry.StartAutoRefresh()
c.Rate.StartAutoRefresh()
diff --git a/core/cfg/cfg.go b/core/cfg/cfg.go
index aaae64d..193f092 100644
--- a/core/cfg/cfg.go
+++ b/core/cfg/cfg.go
@@ -19,6 +19,7 @@ const DefaultSpotLifetime = 10 * time.Minute
var Default = Data{
LogDirectory: "$HOME/",
+ HamDXMapPort: 17300,
Station: pb.Station{
Callsign: "DL0ABC",
Operator: "DL1ABC",
@@ -141,6 +142,7 @@ func AbsoluteFilename() string {
type Data struct {
LogDirectory string `json:"log_directory"`
+ HamDXMapPort int `json:"ham_dx_map_port"`
Station pb.Station `json:"station"`
Contest pb.Contest `json:"contest"`
Radios []core.Radio `json:"radios"`
@@ -159,6 +161,10 @@ func (c *LoadedConfiguration) LogDirectory() string {
return os.ExpandEnv(c.data.LogDirectory)
}
+func (c *LoadedConfiguration) HamDXMapPort() int {
+ return c.data.HamDXMapPort
+}
+
func (c *LoadedConfiguration) Station() core.Station {
result, err := pb.ToStation(c.data.Station)
if err != nil {
diff --git a/core/core.go b/core/core.go
index e343163..d49306d 100644
--- a/core/core.go
+++ b/core/core.go
@@ -1025,6 +1025,7 @@ const (
DXCCService
SCPService
CallHistoryService
+ MapService
)
type ServiceStatusListener interface {
@@ -1038,3 +1039,11 @@ func (f ServiceStatusListenerFunc) StatusChanged(service Service, available bool
}
type AsyncRunner func(func())
+
+type CallsignEnteredListener interface {
+ CallsignEntered(callsign string)
+}
+
+type CallsignLoggedListener interface {
+ CallsignLogged(callsign string, frequency Frequency)
+}
diff --git a/core/entry/entry.go b/core/entry/entry.go
index 2f98137..4b4a7b9 100644
--- a/core/entry/entry.go
+++ b/core/entry/entry.go
@@ -147,6 +147,22 @@ func (c *Controller) Notify(listener any) {
c.listeners = append(c.listeners, listener)
}
+func (c *Controller) emitCallsignEntered(callsign string) {
+ for _, l := range c.listeners {
+ if listener, ok := l.(core.CallsignEnteredListener); ok {
+ listener.CallsignEntered(callsign)
+ }
+ }
+}
+
+func (c *Controller) emitCallsignLogged(callsign string, frequency core.Frequency) {
+ for _, l := range c.listeners {
+ if listener, ok := l.(core.CallsignLoggedListener); ok {
+ listener.CallsignLogged(callsign, frequency)
+ }
+ }
+}
+
func (c *Controller) SetView(view View) {
if view == nil {
c.view = &nullView{}
@@ -525,6 +541,7 @@ func (c *Controller) SendQuestion() {
}
func (c *Controller) enterCallsign(s string) {
+ c.emitCallsignEntered(c.input.callsign)
if c.callinfo != nil {
c.callinfo.ShowInfo(c.input.callsign, c.selectedBand, c.selectedMode, c.input.theirExchange)
}
@@ -674,6 +691,7 @@ func (c *Controller) Log() {
}
c.logbook.Log(qso)
+ c.emitCallsignLogged(qso.Callsign.String(), qso.Frequency)
if c.workmode == core.SearchPounce {
spot := core.Spot{
diff --git a/core/hamdxmap/hamdxmap.go b/core/hamdxmap/hamdxmap.go
new file mode 100644
index 0000000..5c5c0cf
--- /dev/null
+++ b/core/hamdxmap/hamdxmap.go
@@ -0,0 +1,73 @@
+package hamdxmap
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+
+ "github.com/ftl/godxmap"
+ "github.com/ftl/hellocontest/core"
+)
+
+type mapServer interface {
+ Serve() error
+ Close() error
+ ShowPartialCall(string)
+ ShowLoggedCall(string, float64)
+}
+
+type HamDXMap struct {
+ server mapServer
+ closed chan struct{}
+}
+
+func NewHamDXMap(port int) *HamDXMap {
+ result := &HamDXMap{
+ closed: make(chan struct{}),
+ }
+
+ if port > 0 {
+ result.server = godxmap.NewServer(fmt.Sprintf("localhost:%d", port))
+ } else {
+ result.server = &nullServer{}
+ }
+
+ go func() {
+ err := result.server.Serve()
+ if err != nil && err != http.ErrServerClosed {
+ log.Printf("cannot serve HamDXMap server: %v", err)
+ }
+ close(result.closed)
+ }()
+
+ return result
+}
+
+func (m *HamDXMap) Stop() {
+ err := m.server.Close()
+ if err != nil {
+ log.Printf("cannot close HamDXMap server: %v", err)
+ }
+}
+
+func (m *HamDXMap) WhenStopped(callback func()) {
+ go func() {
+ <-m.closed
+ callback()
+ }()
+}
+
+func (m *HamDXMap) CallsignEntered(callsign string) {
+ m.server.ShowPartialCall(callsign)
+}
+
+func (m *HamDXMap) CallsignLogged(callsign string, frequency core.Frequency) {
+ m.server.ShowLoggedCall(callsign, float64(frequency/1000.0))
+}
+
+type nullServer struct{}
+
+func (s *nullServer) Serve() error { return nil }
+func (s *nullServer) Close() error { return nil }
+func (s *nullServer) ShowPartialCall(string) {}
+func (s *nullServer) ShowLoggedCall(string, float64) {}
diff --git a/go.mod b/go.mod
index a99dedf..4db6a47 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/ftl/hellocontest
-go 1.20
+go 1.22.3
// replace github.com/ftl/cabrillo => ../cabrillo
@@ -37,11 +37,13 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/ftl/godxmap v1.0.0 // indirect
github.com/ftl/localcopy v0.0.0-20190616142648-8915fb81f0d9 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
+ golang.org/x/net v0.25.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index f234f24..91a29b0 100644
--- a/go.sum
+++ b/go.sum
@@ -10,6 +10,8 @@ github.com/ftl/conval v0.7.3 h1:RZCR9pWZa0yaQF4O241B5pq1E+RPWjNiGGx7CuXh45E=
github.com/ftl/conval v0.7.3/go.mod h1:Dzx7lU1NMWXgue6ZBRNqNuhVN1kgZb4mu3PQ9hwB1XY=
github.com/ftl/gmtry v0.0.0-20201120192810-fa4a1b99fc04 h1:S7z3LXqDYk4avXKj+B2orGWquOo/A+1ZJbcUPx0duLo=
github.com/ftl/gmtry v0.0.0-20201120192810-fa4a1b99fc04/go.mod h1:AQpbHYBSPV1Bc1nqG8vv8BK3qxXMZIn32OQBi/4A7Sc=
+github.com/ftl/godxmap v1.0.0 h1:5T1SYRtTd49AoISdFGvvRWcqJ9kFA1fzdx6yWJT54zM=
+github.com/ftl/godxmap v1.0.0/go.mod h1:Cl5gtnJFMPtVktcghGTaGeVSFQ1tlZFzkI3p/LylrdI=
github.com/ftl/hamradio v0.2.10 h1:3PpmNpYPkap4z6sbYJjXpL++ZOV4/Xu6aUYlcbclj1w=
github.com/ftl/hamradio v0.2.10/go.mod h1:BvA+ni3sOKmrIJpLt6f2sYK9vc3VfihZm4x0h8kzOPw=
github.com/ftl/localcopy v0.0.0-20190616142648-8915fb81f0d9 h1:ORI3EUKpLTsfA372C6xpuZFDXw+ckmCzLaCcJvakG24=
@@ -62,6 +64,8 @@ github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqa
github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
+golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
+golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
diff --git a/ui/glade/contest.glade b/ui/glade/contest.glade
index f54f90f..8d4ead3 100644
--- a/ui/glade/contest.glade
+++ b/ui/glade/contest.glade
@@ -2566,7 +2566,7 @@ For more details see <a href="https://github.com/ftl/hellocontest/wiki/Main-W
-
+
+
+
+
+ 5
+ 0
+
+
False
diff --git a/ui/statusView.go b/ui/statusView.go
index 4674fd5..502e414 100644
--- a/ui/statusView.go
+++ b/ui/statusView.go
@@ -17,6 +17,7 @@ type statusView struct {
dxccLabel *gtk.Label
scpLabel *gtk.Label
callHistoryLabel *gtk.Label
+ mapLabel *gtk.Label
}
const (
@@ -34,6 +35,7 @@ func setupStatusView(builder *gtk.Builder, colors colorProvider) *statusView {
result.dxccLabel = getUI(builder, "dxccStatusLabel").(*gtk.Label)
result.scpLabel = getUI(builder, "scpStatusLabel").(*gtk.Label)
result.callHistoryLabel = getUI(builder, "callHistoryStatusLabel").(*gtk.Label)
+ result.mapLabel = getUI(builder, "mapStatusLabel").(*gtk.Label)
style := result.indicatorStyle(false)
setStyledText(result.radioLabel, style, "Radio")
@@ -41,6 +43,7 @@ func setupStatusView(builder *gtk.Builder, colors colorProvider) *statusView {
setStyledText(result.dxccLabel, style, "DXCC")
setStyledText(result.scpLabel, style, "SCP")
setStyledText(result.callHistoryLabel, style, "CH")
+ setStyledText(result.mapLabel, style, "Map")
return result
}
@@ -79,6 +82,8 @@ func (v *statusView) serviceLabel(service core.Service) (*gtk.Label, string) {
return v.scpLabel, "SCP"
case core.CallHistoryService:
return v.callHistoryLabel, "CH"
+ case core.MapService:
+ return v.mapLabel, "Map"
default:
return nil, ""
}