first commit
This commit is contained in:
0
.vscode/launch.json
vendored
Normal file
0
.vscode/launch.json
vendored
Normal file
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM golang:1.23.1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
COPY config.go config.yml database.go flexradio.go spot.go main.go TCPClient.go TCPServer.go utils.go log.go xml.go ./
|
||||
COPY templates/* .
|
||||
|
||||
RUN go build -o bin main.go
|
||||
|
||||
ENTRYPOINT ["/app/bin"]
|
209
TCPClient.go
Normal file
209
TCPClient.go
Normal file
@@ -0,0 +1,209 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var spotRe *regexp.Regexp = regexp.MustCompile(`DX\sde\s([\w\d]+).*:\s+(\d+.\d)\s+([\w\d\/]+)\s+(CW|SSB|FT8|FT4|RTTY|USB|LSB)?\s+(.*)\s\s\s+([\d]+\w{1})`)
|
||||
var defaultLoginRe *regexp.Regexp = regexp.MustCompile("[\\w\\d-_]+ login:")
|
||||
var defaultPasswordRe *regexp.Regexp = regexp.MustCompile("Password:")
|
||||
|
||||
type TCPClient struct {
|
||||
Login string
|
||||
Password string
|
||||
Address string
|
||||
Port string
|
||||
LoggedIn bool
|
||||
Timeout time.Duration
|
||||
LogWriter *bufio.Writer
|
||||
Reader *bufio.Reader
|
||||
Writer *bufio.Writer
|
||||
Scanner *bufio.Scanner
|
||||
Mutex sync.Mutex
|
||||
Conn net.Conn
|
||||
TCPServer TCPServer
|
||||
MsgChan chan string
|
||||
CmdChan chan string
|
||||
SpotChanToFlex chan TelnetSpot
|
||||
SpotChanToHTTPServer chan TelnetSpot
|
||||
Log *log.Logger
|
||||
Config *Config
|
||||
Countries Countries
|
||||
LoginRe *regexp.Regexp
|
||||
PasswordRe *regexp.Regexp
|
||||
}
|
||||
|
||||
func NewTCPClient(TCPServer *TCPServer, Countries Countries) *TCPClient {
|
||||
return &TCPClient{
|
||||
Address: Cfg.Cluster.Server,
|
||||
Port: Cfg.Cluster.Port,
|
||||
Login: Cfg.Cluster.Login,
|
||||
Password: Cfg.Cluster.Password,
|
||||
MsgChan: TCPServer.MsgChan,
|
||||
CmdChan: TCPServer.CmdChan,
|
||||
SpotChanToFlex: make(chan TelnetSpot, 100),
|
||||
TCPServer: *TCPServer,
|
||||
SpotChanToHTTPServer: make(chan TelnetSpot, 100),
|
||||
Countries: Countries,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TCPClient) setDefaultParams() {
|
||||
if c.Timeout == 0 {
|
||||
c.Timeout = 600 * time.Second
|
||||
}
|
||||
if c.LogWriter == nil {
|
||||
c.LogWriter = bufio.NewWriter(os.Stdout)
|
||||
}
|
||||
c.LoggedIn = false
|
||||
|
||||
if c.LoginRe == nil {
|
||||
c.LoginRe = defaultLoginRe
|
||||
}
|
||||
|
||||
if c.PasswordRe == nil {
|
||||
c.PasswordRe = defaultPasswordRe
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TCPClient) StartClient() {
|
||||
var err error
|
||||
c.setDefaultParams()
|
||||
|
||||
c.Conn, err = net.Dial("tcp", c.Address+":"+c.Port)
|
||||
if err != nil {
|
||||
Log.Error("Cannot connect to Telnet Client:", err)
|
||||
}
|
||||
|
||||
c.Reader = bufio.NewReader(c.Conn)
|
||||
c.Writer = bufio.NewWriter(c.Conn)
|
||||
|
||||
go func() {
|
||||
for message := range c.TCPServer.CmdChan {
|
||||
Log.Infof("Received Command: %s", message)
|
||||
c.Write([]byte(message + "\r\n"))
|
||||
}
|
||||
}()
|
||||
|
||||
go c.ReadLine()
|
||||
}
|
||||
|
||||
func (c *TCPClient) Close() {
|
||||
c.Writer.Write([]byte("bye\r\n"))
|
||||
}
|
||||
|
||||
func (c *TCPClient) SetFilters() {
|
||||
if Cfg.Cluster.FT8 {
|
||||
c.Write([]byte("set/ft8\r\n"))
|
||||
Log.Info("FT8: On")
|
||||
}
|
||||
|
||||
if Cfg.Cluster.Skimmer {
|
||||
c.Write([]byte("set/skimmer\r\n"))
|
||||
Log.Info("Skimmer: On")
|
||||
}
|
||||
|
||||
if Cfg.Cluster.FT4 {
|
||||
c.Write([]byte("set/ft4\r\n"))
|
||||
Log.Info("FT4: On")
|
||||
}
|
||||
|
||||
if !Cfg.Cluster.FT8 {
|
||||
c.Write([]byte("set/noft8\r\n"))
|
||||
Log.Info("FT8: Off")
|
||||
}
|
||||
|
||||
if !Cfg.Cluster.FT4 {
|
||||
c.Write([]byte("set/noft4\r\n"))
|
||||
Log.Info("FT4: Off")
|
||||
}
|
||||
|
||||
if !Cfg.Cluster.Skimmer {
|
||||
c.Write([]byte("set/noskimmer\r\n"))
|
||||
Log.Info("Skimmer: Off")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TCPClient) ReadLine() {
|
||||
|
||||
for {
|
||||
|
||||
if !c.LoggedIn {
|
||||
message, err := c.Reader.ReadBytes(':')
|
||||
if err != nil {
|
||||
Log.Errorf("Error reading message: %s", err)
|
||||
c.Conn.Close()
|
||||
c.StartClient()
|
||||
}
|
||||
|
||||
if strings.Contains(string(message), Cfg.Cluster.LoginPrompt) || strings.Contains(string(message), "login:") {
|
||||
time.Sleep(time.Second * 1)
|
||||
Log.Debug("Found login prompt...sending callsign")
|
||||
c.Write([]byte(c.Login + "\n\r"))
|
||||
c.LoggedIn = true
|
||||
Log.Infof("Connected to DX cluster %s:%s", Cfg.Cluster.Server, Cfg.Cluster.Port)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if c.LoggedIn {
|
||||
message, err := c.Reader.ReadBytes('\n')
|
||||
messageString := string(message)
|
||||
|
||||
if messageString != "" {
|
||||
if err != nil {
|
||||
Log.Errorf("Error reading message: %s", err)
|
||||
c.Conn.Close()
|
||||
c.StartClient()
|
||||
}
|
||||
|
||||
if strings.Contains(messageString, "password") {
|
||||
Log.Debug("Found password prompt...sending password...")
|
||||
c.Write([]byte(c.Password + "\r\n"))
|
||||
}
|
||||
|
||||
if strings.Contains(messageString, "Hello") || strings.Contains(messageString, "Welcome") {
|
||||
go c.SetFilters()
|
||||
if Cfg.Cluster.Command != "" {
|
||||
c.Write([]byte(Cfg.Cluster.Command + "\n\r"))
|
||||
Log.Debugf("Sending Command: %s", Cfg.Cluster.Command)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(messageString, "Error reading from server: read tcp") {
|
||||
Log.Error("Disconnected from Telnet Server, reconnecting")
|
||||
c.Close()
|
||||
c.StartClient()
|
||||
}
|
||||
|
||||
if strings.Contains(messageString, "DX") {
|
||||
ProcessTelnetSpot(spotRe, messageString, c.SpotChanToFlex, c.SpotChanToHTTPServer, c.Countries)
|
||||
}
|
||||
|
||||
// Send the spot message to TCP server
|
||||
c.MsgChan <- messageString
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write sends raw data to remove telnet server
|
||||
func (c *TCPClient) Write(data []byte) (n int, err error) {
|
||||
Mutex.Lock()
|
||||
n, err = c.Writer.Write(data)
|
||||
if err != nil {
|
||||
Log.Errorf("Error while sending command to telnet client: %s", err)
|
||||
} else {
|
||||
err = c.Writer.Flush()
|
||||
}
|
||||
Mutex.Unlock()
|
||||
return
|
||||
}
|
139
TCPServer.go
Normal file
139
TCPServer.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
err error
|
||||
)
|
||||
|
||||
type TCPServer struct {
|
||||
Address string
|
||||
Port string
|
||||
Clients map[net.Conn]bool
|
||||
Mutex *sync.Mutex
|
||||
LogWriter *bufio.Writer
|
||||
Reader *bufio.Reader
|
||||
Writer *bufio.Writer
|
||||
Conn net.Conn
|
||||
Listener net.Listener
|
||||
MsgChan chan string
|
||||
CmdChan chan string
|
||||
Log *log.Logger
|
||||
Config *Config
|
||||
MessageSent int
|
||||
}
|
||||
|
||||
func NewTCPServer(address string, port string) *TCPServer {
|
||||
return &TCPServer{
|
||||
Address: address,
|
||||
Port: port,
|
||||
Clients: make(map[net.Conn]bool),
|
||||
MsgChan: make(chan string, 100),
|
||||
CmdChan: make(chan string),
|
||||
Mutex: new(sync.Mutex),
|
||||
MessageSent: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TCPServer) StartServer() {
|
||||
s.LogWriter = bufio.NewWriter(os.Stdout)
|
||||
s.Listener, err = net.Listen("tcp", Cfg.TelnetServer.Host+":"+Cfg.TelnetServer.Port)
|
||||
if err != nil {
|
||||
Log.Info("Could not create telnet server")
|
||||
}
|
||||
|
||||
defer s.Listener.Close()
|
||||
|
||||
Log.Infof("Telnet server listening on %s:%s", Cfg.TelnetServer.Host, Cfg.TelnetServer.Port)
|
||||
|
||||
go func() {
|
||||
for message := range s.MsgChan {
|
||||
s.broadcastMessage(message)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
s.Conn, err = s.Listener.Accept()
|
||||
Log.Info("Client connected: ", s.Conn.RemoteAddr().String())
|
||||
if err != nil {
|
||||
Log.Error("Could not accept connections to telnet server")
|
||||
continue
|
||||
}
|
||||
s.Mutex.Lock()
|
||||
s.Clients[s.Conn] = true
|
||||
s.Mutex.Unlock()
|
||||
|
||||
go s.handleConnection()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TCPServer) handleConnection() {
|
||||
s.Conn.Write([]byte("Welcome to the FlexDXCluster telnet server! Type 'bye' to exit.\n"))
|
||||
|
||||
s.Reader = bufio.NewReader(s.Conn)
|
||||
s.Writer = bufio.NewWriter(s.Conn)
|
||||
|
||||
for {
|
||||
|
||||
message, err := s.Reader.ReadString('\n')
|
||||
if err != nil {
|
||||
s.Mutex.Lock()
|
||||
delete(s.Clients, s.Conn)
|
||||
s.Mutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
message = strings.TrimSpace(message)
|
||||
|
||||
// if message is by then disconnect
|
||||
if message == "bye" {
|
||||
s.Mutex.Lock()
|
||||
delete(s.Clients, s.Conn)
|
||||
s.Mutex.Unlock()
|
||||
s.Conn.Close()
|
||||
Log.Infof("client %s disconnected", s.Conn.RemoteAddr().String())
|
||||
}
|
||||
|
||||
if strings.Contains(message, "DX") || strings.Contains(message, "SH/DX") || strings.Contains(message, "set") || strings.Contains(message, "SET") {
|
||||
// send DX spot to the client
|
||||
s.CmdChan <- message
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TCPServer) Write(message string) (n int, err error) {
|
||||
_, err = s.Writer.Write([]byte(message))
|
||||
if err == nil {
|
||||
err = s.Writer.Flush()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *TCPServer) broadcastMessage(message string) {
|
||||
s.Mutex.Lock()
|
||||
defer s.Mutex.Unlock()
|
||||
if len(s.Clients) > 0 {
|
||||
if s.MessageSent == 0 {
|
||||
time.Sleep(3 * time.Second)
|
||||
s.MessageSent += 1
|
||||
}
|
||||
for client := range s.Clients {
|
||||
_, err := client.Write([]byte(message + "\r\n"))
|
||||
s.MessageSent += 1
|
||||
if err != nil {
|
||||
fmt.Println("Error while sending message to clients:", client.RemoteAddr())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
111
config.go
Normal file
111
config.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var Cfg *Config
|
||||
|
||||
type Config struct {
|
||||
General struct {
|
||||
DeleteLogFileAtStart bool `yaml:"delete_log_file_at_start"`
|
||||
LogToFile bool `yaml:"log_to_file"`
|
||||
Callsign string `yaml:"callsign"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
TelnetServer bool `yaml:"telnetserver"`
|
||||
FlexRadioSpot bool `yaml:"flexradiospot"`
|
||||
SpotColorNewEntity string `yaml:"spot_color_new_entity"`
|
||||
BackgroundColorNewEntity string `yaml:"background_color_new_entity"`
|
||||
SpotColorNewBand string `yaml:"spot_color_new_band"`
|
||||
BackgroundColorNewBand string `yaml:"background_color_new_band"`
|
||||
SpotColorNewMode string `yaml:"spot_color_new_mode"`
|
||||
BackgroundColorNewMode string `yaml:"background_color_new_mode"`
|
||||
SpotColorNewBandMode string `yaml:"spot_color_new_band_mode"`
|
||||
BackgroundColorNewBandMode string `yaml:"background_color_new_band_mode"`
|
||||
SpotColorNewSlot string `yaml:"spot_color_new_slot"`
|
||||
BackgroundColorNewSlot string `yaml:"background_color_new_slot"`
|
||||
SpotColorMyCallsign string `yaml:"spot_color_my_callsign"`
|
||||
BackgroundColorMyCallsign string `yaml:"background_color_my_callsign"`
|
||||
SpotColorWorked string `yaml:"spot_color_worked"`
|
||||
BackgroundColorWorked string `yaml:"background_color_worked"`
|
||||
} `yaml:"general"`
|
||||
|
||||
Database struct {
|
||||
MySQL bool `yaml:"mysql"`
|
||||
SQLite bool `yaml:"sqlite"`
|
||||
MySQLUser string `yaml:"mysql_db_user"`
|
||||
MySQLPassword string `yaml:"mysql_db_password"`
|
||||
MySQLDbName string `yaml:"mysql_db_name"`
|
||||
MySQLHost string `yaml:"mysql_host"`
|
||||
MySQLPort string `yaml:"mysql_port"`
|
||||
} `yaml:"database"`
|
||||
|
||||
SQLite struct {
|
||||
SQLitePath string `yaml:"sqlite_path"`
|
||||
} `yaml:"sqlite"`
|
||||
|
||||
Cluster struct {
|
||||
Server string `yaml:"server"`
|
||||
Port string `yaml:"port"`
|
||||
Login string `yaml:"login"`
|
||||
Password string `yaml:"password"`
|
||||
Skimmer bool `yaml:"skimmer"`
|
||||
FT8 bool `yaml:"ft8"`
|
||||
FT4 bool `yaml:"ft4"`
|
||||
Command string `yanl:"command"`
|
||||
LoginPrompt string `yaml:"login_prompt"`
|
||||
} `yaml:"cluster"`
|
||||
|
||||
Flex struct {
|
||||
Discover bool `yaml:"discovery"`
|
||||
IP string `yaml:"ip"`
|
||||
SpotLife string `yaml:"spot_life"`
|
||||
} `yaml:"flex"`
|
||||
|
||||
TelnetServer struct {
|
||||
Host string `yaml:"host"`
|
||||
Port string `yaml:"port"`
|
||||
} `yaml:"telnetserver"`
|
||||
|
||||
Gotify struct {
|
||||
Enable bool `yaml:"enable"`
|
||||
URL string `yaml:"url"`
|
||||
Token string `yaml:"token"`
|
||||
NewDXCC bool `yaml:"NewDXCC"`
|
||||
NewBand bool `yaml:"NewBand"`
|
||||
NewMode bool `yaml:"NewMode"`
|
||||
NewBandAndMode bool `yaml:"NewBandAndMode"`
|
||||
} `yaml:"gotify"`
|
||||
}
|
||||
|
||||
func NewConfig(configPath string) *Config {
|
||||
Cfg = &Config{}
|
||||
|
||||
file, err := os.Open(configPath)
|
||||
if err != nil {
|
||||
log.Println("could not open config file")
|
||||
}
|
||||
defer file.Close()
|
||||
d := yaml.NewDecoder(file)
|
||||
|
||||
if err := d.Decode(&Cfg); err != nil {
|
||||
log.Println("could not decod config file")
|
||||
}
|
||||
|
||||
return Cfg
|
||||
}
|
||||
|
||||
func ValidateConfigPath(path string) error {
|
||||
s, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if s.IsDir() {
|
||||
return fmt.Errorf("'%s' is a directory, not a normal file", path)
|
||||
}
|
||||
return nil
|
||||
}
|
57
config.yml
Normal file
57
config.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
general:
|
||||
delete_log_file_at_start: true
|
||||
callsign: F4BPO # Log4OM Callsign used to check if you get spotted by someone
|
||||
log_to_file: true
|
||||
log_level: DEBUG # INFO or DEBUG or WARN
|
||||
telnetserver: true # not in use for now
|
||||
flexradiospot: true # not in use for now
|
||||
# Spot colors, if empty then default, colors in HEX AARRGGBB format
|
||||
spot_color_new_entity:
|
||||
background_color_new_entity:
|
||||
spot_color_new_band:
|
||||
background_color_new_band:
|
||||
spot_color_new_mode:
|
||||
background_color_new_mode:
|
||||
spot_color_new_band_mode:
|
||||
background_color_new_band_mode:
|
||||
spot_color_new_slot:
|
||||
background_color_new_slot:
|
||||
spot_color_my_callsign:
|
||||
background_color_my_callsign:
|
||||
spot_color_worked:
|
||||
background_color_worked:
|
||||
database:
|
||||
mysql: false #only one of the two can be true
|
||||
sqlite: true
|
||||
mysql_db_user: rouggy
|
||||
mysql_db_password: 89DGgg290379
|
||||
mysql_db_name: log_f4bpo
|
||||
mysql_host: 10.10.10.15
|
||||
mysql_port: 3306
|
||||
sqlite:
|
||||
sqlite_path: 'C:\Perso\Seafile\Radio\Logs\Log4OM\F4BPO.SQLite' # SQLite Db oath of Log4OM
|
||||
cluster:
|
||||
server: cluster.f4bpo.com # dxc.k0xm.net dxc.sm7iun.se
|
||||
port: 7300
|
||||
login: f4bpo
|
||||
password: 89DGgg
|
||||
skimmer: true
|
||||
ft8: false
|
||||
ft4: false
|
||||
command: "SET/FILTER DOC/PASS 1A,3A,4O,9A,9H,C3,CT,CU,DL,E7,EA,EA6,EI,ER,ES,EU,F,G,GD,GI,GJ,GM,GU,GW,HA,HB,HB0,HV,I,IS,IT9,JW,JX,LA,LX,LY,LZ,OE,OH,OH0,OJ0,OK,OM,ON,OY,OZ,PA,S5,SM,SP,SV,SV5,SV9,T7,TA1,TF,TK,UA,UR,YL,YO,YU,Z6,Z3" #"SET/FILTER DOC/PASS 1A,3A,4O,9A,9H,C3,CT,CU,DL,E7,EA,EA6,EI,ER,ES,EU,F,G,GD,GI,GJ,GM,GU,GW,HA,HB,HB0,HV,I,IS,IT9,JW,JX,LA,LX,LY,LZ,OE,OH,OH0,OJ0,OK,OM,ON,OY,OZ,PA,S5,SM,SP,SV,SV5,SV9,T7,TA1,TF,TK,UA,UR,YL,YO,YU,Z6,Z3,ZA,ZB"
|
||||
login_prompt: "login:"
|
||||
flex:
|
||||
discovery: false # Radio must be on same LAN than the program
|
||||
ip: 82.67.157.19 # if discovery is true no need to put an IP
|
||||
spot_life: 600 #seconds
|
||||
telnetserver: # Log4OM must be connected to this server ie: localhost:7301 if on same machine as this program else ip:7301
|
||||
host: 0.0.0.0
|
||||
port: 7301
|
||||
gotify:
|
||||
enable: true
|
||||
url: https://gotify.rouggy.com/message
|
||||
token: ALaGS4MVMWTEMcP
|
||||
NewDXCC: true
|
||||
NewBand: false
|
||||
NewMode: false
|
||||
NewBandAndMode: false
|
16888
country.xml
Normal file
16888
country.xml
Normal file
File diff suppressed because one or more lines are too long
470
database.go
Normal file
470
database.go
Normal file
@@ -0,0 +1,470 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Contact struct {
|
||||
Callsign string
|
||||
Band string
|
||||
Mode string
|
||||
DXCC string
|
||||
StationCallsign string
|
||||
Country string
|
||||
}
|
||||
|
||||
type Spotter struct {
|
||||
Spotter string
|
||||
NumberofSpots string
|
||||
}
|
||||
|
||||
type Log4OMContactsRepository struct {
|
||||
db *sql.DB
|
||||
Log *log.Logger
|
||||
}
|
||||
|
||||
type FlexDXClusterRepository struct {
|
||||
db *sql.DB
|
||||
Log *log.Logger
|
||||
}
|
||||
|
||||
func NewLog4OMContactsRepository(filePath string) *Log4OMContactsRepository {
|
||||
|
||||
if Cfg.Database.MySQL {
|
||||
db, err := sql.Open("mysql", Cfg.Database.MySQLUser+":"+Cfg.Database.MySQLPassword+"@tcp("+Cfg.Database.MySQLHost+":"+Cfg.Database.MySQLPort+")/"+Cfg.Database.MySQLDbName)
|
||||
if err != nil {
|
||||
Log.Errorf("Cannot open db", err)
|
||||
}
|
||||
|
||||
return &Log4OMContactsRepository{
|
||||
db: db,
|
||||
Log: Log}
|
||||
|
||||
} else if Cfg.Database.SQLite {
|
||||
db, err := sql.Open("sqlite3", filePath)
|
||||
if err != nil {
|
||||
Log.Errorf("Cannot open db", err)
|
||||
}
|
||||
_, err = db.Exec("PRAGMA journal_mode=WAL")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &Log4OMContactsRepository{
|
||||
db: db,
|
||||
Log: Log}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewFlexDXDatabase(filePath string) *FlexDXClusterRepository {
|
||||
|
||||
db, err := sql.Open("sqlite3", filePath)
|
||||
if err != nil {
|
||||
fmt.Println("Cannot open db", err)
|
||||
}
|
||||
|
||||
Log.Debugln("Opening SQLite database")
|
||||
|
||||
_, err = db.ExecContext(
|
||||
context.Background(),
|
||||
`CREATE TABLE IF NOT EXISTS "spots" (
|
||||
"id" INTEGER NOT NULL UNIQUE,
|
||||
"commandNumber" INTEGER NOT NULL UNIQUE,
|
||||
"flexSpotNumber" INTEGER,
|
||||
"dx" TEXT NOT NULL,
|
||||
"freqMhz" TEXT,
|
||||
"freqHz" TEXT,
|
||||
"band" TEXT,
|
||||
"mode" TEXT,
|
||||
"spotter" TEXT,
|
||||
"flexMode" TEXT,
|
||||
"source" TEXT,
|
||||
"time" TEXT,
|
||||
"timestamp" INTEGER,
|
||||
"lifeTime" TEXT,
|
||||
"priority" TEXT,
|
||||
"comment" TEXT,
|
||||
"color" TEXT,
|
||||
"backgroundColor" TEXT,
|
||||
"countryName" TEXT,
|
||||
"dxcc" TEXT,
|
||||
"newDXCC" INTEGER DEFAULT 0,
|
||||
"newBand" INTEGER DEFAULT 0,
|
||||
"newMode" INTEGER DEFAULT 0,
|
||||
"newSlot" INTEGER DEFAULT 0,
|
||||
"worked" INTEGER DEFAULT 0,
|
||||
PRIMARY KEY("id" AUTOINCREMENT)
|
||||
)`,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Warn("Cannot create table", err)
|
||||
}
|
||||
|
||||
return &FlexDXClusterRepository{
|
||||
db: db,
|
||||
Log: Log,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Log4OMContactsRepository) CountEntries() int {
|
||||
var contacts int
|
||||
err := r.db.QueryRow("SELECT COUNT(*) FROM log").Scan(&contacts)
|
||||
if err != nil {
|
||||
log.Error("could not query database", err)
|
||||
}
|
||||
|
||||
return contacts
|
||||
}
|
||||
|
||||
func (r *Log4OMContactsRepository) ListByCountry(countryID string, contactsChan chan []Contact, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE dxcc = ?", countryID)
|
||||
if err != nil {
|
||||
log.Error("could not query database", err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
contacts := []Contact{}
|
||||
for rows.Next() {
|
||||
c := Contact{}
|
||||
if err := rows.Scan(&c.Callsign, &c.Band, &c.Mode, &c.DXCC, &c.StationCallsign, &c.Country); err != nil {
|
||||
log.Error("could not query database", err)
|
||||
}
|
||||
contacts = append(contacts, c)
|
||||
}
|
||||
contactsChan <- contacts
|
||||
}
|
||||
|
||||
func (r *Log4OMContactsRepository) ListByCountryMode(countryID string, mode string, contactsModeChan chan []Contact, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
if mode == "USB" || mode == "LSB" || mode == "SSB" {
|
||||
|
||||
rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE dxcc = ? AND (mode = ? OR mode = ? OR mode = ?)", countryID, "USB", "LSB", "SSB")
|
||||
if err != nil {
|
||||
log.Error("could not query database", err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
contacts := []Contact{}
|
||||
for rows.Next() {
|
||||
c := Contact{}
|
||||
if err := rows.Scan(&c.Callsign, &c.Band, &c.Mode, &c.DXCC, &c.StationCallsign, &c.Country); err != nil {
|
||||
log.Error("could not query database", err)
|
||||
|
||||
}
|
||||
contacts = append(contacts, c)
|
||||
}
|
||||
contactsModeChan <- contacts
|
||||
|
||||
} else {
|
||||
|
||||
rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE dxcc = ? AND mode = ?", countryID, mode)
|
||||
if err != nil {
|
||||
log.Error("could not query the database", err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
contacts := []Contact{}
|
||||
for rows.Next() {
|
||||
c := Contact{}
|
||||
if err := rows.Scan(&c.Callsign, &c.Band, &c.Mode, &c.DXCC, &c.StationCallsign, &c.Country); err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
}
|
||||
contacts = append(contacts, c)
|
||||
}
|
||||
contactsModeChan <- contacts
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Log4OMContactsRepository) ListByCountryModeBand(countryID string, band string, mode string, contactsModeBandChan chan []Contact, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
if mode == "USB" || mode == "LSB" {
|
||||
|
||||
rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE dxcc = ? AND (mode = ? OR mode = ?) AND band = ?", countryID, "USB", "LSB", band)
|
||||
if err != nil {
|
||||
log.Error("could not query database", err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
contacts := []Contact{}
|
||||
for rows.Next() {
|
||||
c := Contact{}
|
||||
if err := rows.Scan(&c.Callsign, &c.Band, &c.Mode, &c.DXCC, &c.StationCallsign, &c.Country); err != nil {
|
||||
log.Error("could not query database", err)
|
||||
|
||||
}
|
||||
contacts = append(contacts, c)
|
||||
}
|
||||
contactsModeBandChan <- contacts
|
||||
|
||||
} else {
|
||||
|
||||
rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE dxcc = ? AND mode = ? AND band = ?", countryID, mode, band)
|
||||
if err != nil {
|
||||
log.Error("could not query the database", err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
contacts := []Contact{}
|
||||
for rows.Next() {
|
||||
c := Contact{}
|
||||
if err := rows.Scan(&c.Callsign, &c.Band, &c.Mode, &c.DXCC, &c.StationCallsign, &c.Country); err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
}
|
||||
contacts = append(contacts, c)
|
||||
}
|
||||
contactsModeBandChan <- contacts
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Log4OMContactsRepository) ListByCountryBand(countryID string, band string, contactsBandChan chan []Contact, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE dxcc = ? AND band = ?", countryID, band)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
contacts := []Contact{}
|
||||
for rows.Next() {
|
||||
c := Contact{}
|
||||
if err := rows.Scan(&c.Callsign, &c.Band, &c.Mode, &c.DXCC, &c.StationCallsign, &c.Country); err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
}
|
||||
contacts = append(contacts, c)
|
||||
}
|
||||
contactsBandChan <- contacts
|
||||
}
|
||||
|
||||
func (r *Log4OMContactsRepository) ListByCallSign(callSign string, band string, mode string, contactsCallChan chan []Contact, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE callsign = ? AND band = ? AND mode = ?", callSign, band, mode)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
contacts := []Contact{}
|
||||
for rows.Next() {
|
||||
c := Contact{}
|
||||
if err := rows.Scan(&c.Callsign, &c.Band, &c.Mode, &c.DXCC, &c.StationCallsign, &c.Country); err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
}
|
||||
contacts = append(contacts, c)
|
||||
}
|
||||
contactsCallChan <- contacts
|
||||
}
|
||||
|
||||
func (r *FlexDXClusterRepository) GetAllSpots(limit string) []FlexSpot {
|
||||
|
||||
Spots := []FlexSpot{}
|
||||
|
||||
var query string
|
||||
|
||||
if limit == "0" {
|
||||
query = "SELECT * from spots ORDER BY id DESC"
|
||||
} else {
|
||||
query = fmt.Sprintf("SELECT * from spots ORDER BY id DESC LIMIT %s", limit)
|
||||
}
|
||||
|
||||
rows, err := r.db.Query(query)
|
||||
|
||||
if err != nil {
|
||||
r.Log.Error(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
s := FlexSpot{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.ID, &s.CommandNumber, &s.FlexSpotNumber, &s.DX, &s.FrequencyMhz, &s.FrequencyHz, &s.Band, &s.Mode, &s.SpotterCallsign, &s.FlexMode, &s.Source, &s.UTCTime, &s.TimeStamp, &s.LifeTime, &s.Priority,
|
||||
&s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
Spots = append(Spots, s)
|
||||
}
|
||||
|
||||
return Spots
|
||||
}
|
||||
|
||||
func (r *FlexDXClusterRepository) GetSpotters() []Spotter {
|
||||
|
||||
sList := []Spotter{}
|
||||
|
||||
rows, err := r.db.Query("select spotter, count(*) as occurences from spots group by spotter order by occurences desc, spotter limit 15")
|
||||
|
||||
if err != nil {
|
||||
r.Log.Error(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
s := Spotter{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.Spotter, &s.NumberofSpots); err != nil {
|
||||
fmt.Println(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
sList = append(sList, s)
|
||||
}
|
||||
|
||||
return sList
|
||||
}
|
||||
|
||||
func (r *FlexDXClusterRepository) FindDXSameBand(spot FlexSpot) (*FlexSpot, error) {
|
||||
rows, err := r.db.Query("SELECT * from spots WHERE dx = ? AND band = ?", spot.DX, spot.Band)
|
||||
if err != nil {
|
||||
r.Log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
s := FlexSpot{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.ID, &s.CommandNumber, &s.FlexSpotNumber, &s.DX, &s.FrequencyMhz, &s.FrequencyHz, &s.Band, &s.Mode, &s.SpotterCallsign, &s.FlexMode, &s.Source, &s.UTCTime, &s.TimeStamp, &s.LifeTime, &s.Priority,
|
||||
&s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func (r *FlexDXClusterRepository) CreateSpot(spot FlexSpot) {
|
||||
query := "INSERT INTO `spots` (`commandNumber`, `flexSpotNumber`, `dx`, `freqMhz`, `freqHz`, `band`, `mode`, `spotter`, `flexMode`, `source`, `time`, `timestamp`, `lifeTime`, `priority`, `comment`, `color`, `backgroundColor`, `countryName`, `dxcc`, `newDXCC`, `newBand`, `newMode`, `newSlot`, `worked`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
insertResult, err := r.db.ExecContext(context.Background(), query, spot.CommandNumber, spot.CommandNumber, spot.DX, spot.FrequencyMhz, spot.FrequencyHz, spot.Band, spot.Mode, spot.SpotterCallsign, spot.FlexMode, spot.Source, spot.UTCTime, time.Now().Unix(), spot.LifeTime, spot.Priority, spot.Comment, spot.Color, spot.BackgroundColor, spot.CountryName, spot.DXCC, spot.NewDXCC, spot.NewBand, spot.NewMode, spot.NewSlot, spot.Worked)
|
||||
if err != nil {
|
||||
Log.Errorf("cannot insert spot in database: %s", err)
|
||||
}
|
||||
|
||||
_, err = insertResult.LastInsertId()
|
||||
if err != nil {
|
||||
Log.Errorf("impossible to retrieve last inserted id: %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (r *FlexDXClusterRepository) UpdateSpotSameBand(spot FlexSpot) error {
|
||||
_, err := r.db.Exec(`UPDATE spots SET commandNumber = ?, DX = ?, freqMhz = ?, freqHz = ?, band = ?, mode = ?, spotter = ?, flexMode = ?, source = ?, time = ?, timestamp = ?, lifeTime = ?, priority = ?, comment = ?, color = ?, backgroundColor = ?, countryName = ?, dxcc = ?, newDXCC = ?, newBand = ?, newMode = ?, newSlot = ?, worked = ? WHERE DX = ? AND band = ?`,
|
||||
spot.CommandNumber, spot.DX, spot.FrequencyMhz, spot.FrequencyHz, spot.Band, spot.Mode, spot.SpotterCallsign, spot.FlexMode, spot.Source, spot.UTCTime, spot.TimeStamp, spot.LifeTime, spot.Priority, spot.Comment, spot.Color, spot.BackgroundColor, spot.CountryName, spot.DXCC, spot.NewDXCC, spot.NewBand, spot.NewMode, spot.NewSlot, spot.Worked, spot.DX, spot.Band)
|
||||
if err != nil {
|
||||
r.Log.Errorf("could not update database: %s", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *FlexDXClusterRepository) FindSpotByCommandNumber(commandNumber string) (*FlexSpot, error) {
|
||||
rows, err := r.db.Query("SELECT * from spots WHERE commandNumber = ?", commandNumber)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
s := FlexSpot{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.ID, &s.CommandNumber, &s.FlexSpotNumber, &s.DX, &s.FrequencyMhz, &s.FrequencyHz, &s.Band, &s.Mode, &s.SpotterCallsign, &s.FlexMode, &s.Source, &s.UTCTime, &s.TimeStamp, &s.LifeTime, &s.Priority,
|
||||
&s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func (r *FlexDXClusterRepository) FindSpotByFlexSpotNumber(spotNumber string) (*FlexSpot, error) {
|
||||
rows, err := r.db.Query("SELECT * from spots WHERE flexSpotNumber = ?", spotNumber)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
s := FlexSpot{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.ID, &s.CommandNumber, &s.FlexSpotNumber, &s.DX, &s.FrequencyMhz, &s.FrequencyHz, &s.Band, &s.Mode, &s.SpotterCallsign, &s.FlexMode, &s.Source, &s.UTCTime, &s.TimeStamp, &s.LifeTime, &s.Priority,
|
||||
&s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func (r *FlexDXClusterRepository) UpdateFlexSpotNumberByID(flexSpotNumber string, spot FlexSpot) (*FlexSpot, error) {
|
||||
flexSpotNumberInt, _ := strconv.Atoi(flexSpotNumber)
|
||||
rows, err := r.db.Query(`UPDATE spots SET flexSpotNumber = ? WHERE id = ? RETURNING *`, flexSpotNumberInt, spot.ID)
|
||||
if err != nil {
|
||||
r.Log.Errorf("could not update database: %s", err)
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
s := FlexSpot{}
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&s.ID, &s.CommandNumber, &s.FlexSpotNumber, &s.DX, &s.FrequencyMhz, &s.FrequencyHz, &s.Band, &s.Mode, &s.SpotterCallsign, &s.FlexMode, &s.Source, &s.UTCTime, &s.TimeStamp, &s.LifeTime, &s.Priority,
|
||||
&s.Comment, &s.Color, &s.BackgroundColor, &s.CountryName, &s.DXCC, &s.NewDXCC, &s.NewBand, &s.NewMode, &s.NewSlot, &s.Worked); err != nil {
|
||||
fmt.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func (r *FlexDXClusterRepository) DeleteSpotByFlexSpotNumber(flexSpotNumber string) {
|
||||
flexSpotNumberInt, _ := strconv.Atoi(flexSpotNumber)
|
||||
query := "DELETE from spots WHERE flexSpotNumber = ?"
|
||||
_, err := r.db.Exec(query, flexSpotNumberInt)
|
||||
if err != nil {
|
||||
r.Log.Errorf("could not delete spot %v from database", flexSpotNumberInt)
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteDatabase(filePath string, log *log.Logger) {
|
||||
_, err := os.Stat(filePath)
|
||||
if !os.IsNotExist(err) {
|
||||
err := os.Remove(filePath)
|
||||
if err != nil {
|
||||
log.Error("could not delete existing database")
|
||||
}
|
||||
log.Debug("deleting existing database")
|
||||
}
|
||||
}
|
378
flexradio.go
Normal file
378
flexradio.go
Normal file
@@ -0,0 +1,378 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var CommandNumber int = 1
|
||||
|
||||
type FlexSpot struct {
|
||||
ID int
|
||||
CommandNumber int
|
||||
FlexSpotNumber int
|
||||
DX string
|
||||
FrequencyMhz string
|
||||
FrequencyHz string
|
||||
Band string
|
||||
Mode string
|
||||
FlexMode string
|
||||
Source string
|
||||
SpotterCallsign string
|
||||
TimeStamp int64
|
||||
UTCTime string
|
||||
LifeTime string
|
||||
Priority string
|
||||
Comment string
|
||||
Color string
|
||||
BackgroundColor string
|
||||
NewDXCC bool
|
||||
NewBand bool
|
||||
NewMode bool
|
||||
NewSlot bool
|
||||
Worked bool
|
||||
CountryName string
|
||||
DXCC string // Added DXCC field
|
||||
}
|
||||
|
||||
type Discovery struct {
|
||||
IP string
|
||||
NickName string
|
||||
Model string
|
||||
Serial string
|
||||
Version string
|
||||
}
|
||||
|
||||
type FlexClient struct {
|
||||
Address string
|
||||
Port string
|
||||
Timeout time.Duration
|
||||
LogWriter *bufio.Writer
|
||||
Reader *bufio.Reader
|
||||
Writer *bufio.Writer
|
||||
Conn *net.TCPConn
|
||||
SpotChanToFlex chan TelnetSpot
|
||||
MsgChan chan string
|
||||
Repo FlexDXClusterRepository
|
||||
TCPServer *TCPServer
|
||||
IsConnected bool
|
||||
}
|
||||
|
||||
func NewFlexClient(repo FlexDXClusterRepository, TCPServer *TCPServer, SpotChanToFlex chan TelnetSpot) *FlexClient {
|
||||
return &FlexClient{
|
||||
Port: "4992",
|
||||
SpotChanToFlex: SpotChanToFlex,
|
||||
MsgChan: TCPServer.MsgChan,
|
||||
Repo: repo,
|
||||
TCPServer: TCPServer,
|
||||
IsConnected: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (fc *FlexClient) StartFlexClient() {
|
||||
|
||||
if Cfg.Flex.IP == "" && !Cfg.Flex.Discover {
|
||||
Log.Errorln("You must either turn FlexRadio Discovery on or provide an IP address for the Flex")
|
||||
|
||||
} else if Cfg.Flex.Discover {
|
||||
ok, d := DiscoverFlexRadio()
|
||||
if ok {
|
||||
fc.Address = d.IP
|
||||
Log.Infof("Found: %s with Nick: %s, Version: %s, Serial: %s - using IP: %s", d.Model, d.NickName, d.Version, d.Serial, d.IP)
|
||||
} else {
|
||||
Log.Errorln("Could not discover any FlexRadio on the network, please provide an IP address in the config file.")
|
||||
fc.StartFlexClient()
|
||||
}
|
||||
} else if Cfg.Flex.IP != "" {
|
||||
fc.Address = Cfg.Flex.IP
|
||||
}
|
||||
|
||||
if fc.Address != "" {
|
||||
addr, err := net.ResolveTCPAddr("tcp", fc.Address+":"+fc.Port)
|
||||
if err != nil {
|
||||
Log.Error("Cannot resolve Telnet Client address")
|
||||
}
|
||||
|
||||
fc.LogWriter = bufio.NewWriter(os.Stdout)
|
||||
|
||||
fc.Timeout = 600 * time.Second
|
||||
|
||||
Log.Debugf("Trying to connect to FlexRadio at %s:%s", fc.Address, fc.Port)
|
||||
|
||||
fc.Conn, err = net.DialTCP("tcp", nil, addr)
|
||||
if err != nil {
|
||||
Log.Errorf("Could not connect to FlexRadio on %s", Cfg.Flex.IP)
|
||||
Log.Error("Retrying to connect to FlexRadio in 5 seconds")
|
||||
time.Sleep(time.Second * 5)
|
||||
fc.StartFlexClient()
|
||||
}
|
||||
Log.Infof("Connected to FlexRadio at %s:%s", fc.Address, fc.Port)
|
||||
fc.IsConnected = true
|
||||
|
||||
go func() {
|
||||
for message := range fc.SpotChanToFlex {
|
||||
fc.SendSpottoFlex(message)
|
||||
}
|
||||
}()
|
||||
|
||||
fc.Reader = bufio.NewReader(fc.Conn)
|
||||
fc.Writer = bufio.NewWriter(fc.Conn)
|
||||
|
||||
err = fc.Conn.SetKeepAlive(true)
|
||||
if err != nil {
|
||||
Log.Error("error while setting keep alive")
|
||||
}
|
||||
|
||||
go fc.ReadLine()
|
||||
|
||||
subSpotAllCmd := fmt.Sprintf("C%v|sub spot all", CommandNumber)
|
||||
fc.Write(subSpotAllCmd)
|
||||
CommandNumber++
|
||||
|
||||
clrSpotAllCmd := fmt.Sprintf("C%v|spot clear", CommandNumber)
|
||||
fc.Write(clrSpotAllCmd)
|
||||
CommandNumber++
|
||||
|
||||
Log.Debug("Subscribed to spot on FlexRadio and Deleted all spots from panadapter")
|
||||
} else {
|
||||
Log.Errorln("You must either turn FlexRadio Discovery on or provide an IP address for the Flex")
|
||||
}
|
||||
}
|
||||
|
||||
func (fc *FlexClient) SendSpottoFlex(spot TelnetSpot) {
|
||||
|
||||
freq := FreqMhztoHz(spot.Frequency)
|
||||
|
||||
flexSpot := FlexSpot{
|
||||
CommandNumber: CommandNumber,
|
||||
DX: spot.DX,
|
||||
FrequencyMhz: freq,
|
||||
FrequencyHz: spot.Frequency,
|
||||
Band: spot.Band,
|
||||
Mode: spot.Mode,
|
||||
Source: "FlexDXCluster",
|
||||
SpotterCallsign: spot.Spotter,
|
||||
TimeStamp: time.Now().Unix(),
|
||||
UTCTime: spot.Time,
|
||||
LifeTime: Cfg.Flex.SpotLife,
|
||||
Comment: spot.Comment,
|
||||
Color: "#ffeaeaea",
|
||||
BackgroundColor: "#ff000000",
|
||||
Priority: "5",
|
||||
NewDXCC: spot.NewDXCC,
|
||||
NewBand: spot.NewBand,
|
||||
NewMode: spot.NewMode,
|
||||
NewSlot: spot.NewSlot,
|
||||
Worked: spot.CallsignWorked,
|
||||
CountryName: spot.CountryName,
|
||||
DXCC: spot.DXCC,
|
||||
}
|
||||
|
||||
flexSpot.Comment = flexSpot.Comment + " [" + flexSpot.Mode + "] [" + flexSpot.SpotterCallsign + "] [" + flexSpot.UTCTime + "]"
|
||||
|
||||
// If new DXCC
|
||||
if spot.NewDXCC {
|
||||
flexSpot.Priority = "1"
|
||||
flexSpot.Comment = flexSpot.Comment + " [New DXCC]"
|
||||
if Cfg.General.SpotColorNewEntity != "" && Cfg.General.BackgroundColorNewEntity != "" {
|
||||
flexSpot.Color = Cfg.General.SpotColorNewEntity
|
||||
flexSpot.BackgroundColor = Cfg.General.BackgroundColorNewEntity
|
||||
} else {
|
||||
flexSpot.Color = "#ff3bf908"
|
||||
flexSpot.BackgroundColor = "#ff000000"
|
||||
}
|
||||
} else if spot.DX == Cfg.General.Callsign {
|
||||
flexSpot.Priority = "1"
|
||||
if Cfg.General.SpotColorMyCallsign != "" && Cfg.General.BackgroundColorMyCallsign != "" {
|
||||
flexSpot.Color = Cfg.General.SpotColorMyCallsign
|
||||
flexSpot.BackgroundColor = Cfg.General.BackgroundColorMyCallsign
|
||||
} else {
|
||||
flexSpot.Color = "#ffff0000"
|
||||
flexSpot.BackgroundColor = "#ff000000"
|
||||
}
|
||||
} else if spot.CallsignWorked {
|
||||
flexSpot.Priority = "5"
|
||||
flexSpot.Comment = flexSpot.Comment + " [Worked]"
|
||||
if Cfg.General.SpotColorWorked != "" && Cfg.General.BackgroundColorWorked != "" {
|
||||
flexSpot.Color = Cfg.General.SpotColorWorked
|
||||
flexSpot.BackgroundColor = Cfg.General.BackgroundColorWorked
|
||||
} else {
|
||||
flexSpot.Color = "#ff000000"
|
||||
flexSpot.BackgroundColor = "#ff00c0c0"
|
||||
}
|
||||
} else if spot.NewMode && spot.NewBand {
|
||||
flexSpot.Priority = "1"
|
||||
flexSpot.Comment = flexSpot.Comment + " [New Band & Mode]"
|
||||
if Cfg.General.SpotColorNewBandMode != "" && Cfg.General.BackgroundColorNewBandMode != "" {
|
||||
flexSpot.Color = Cfg.General.SpotColorNewBandMode
|
||||
flexSpot.BackgroundColor = Cfg.General.BackgroundColorNewBandMode
|
||||
} else {
|
||||
flexSpot.Color = "#ffc603fc"
|
||||
flexSpot.BackgroundColor = "#ff000000"
|
||||
}
|
||||
} else if spot.NewMode && !spot.NewBand {
|
||||
flexSpot.Priority = "2"
|
||||
flexSpot.Comment = flexSpot.Comment + " [New Mode]"
|
||||
if Cfg.General.SpotColorNewMode != "" && Cfg.General.BackgroundColorNewMode != "" {
|
||||
flexSpot.Color = Cfg.General.SpotColorNewMode
|
||||
flexSpot.BackgroundColor = Cfg.General.BackgroundColorNewMode
|
||||
} else {
|
||||
flexSpot.Color = "#fff9a908"
|
||||
flexSpot.BackgroundColor = "#ff000000"
|
||||
}
|
||||
} else if spot.NewBand && !spot.NewMode {
|
||||
flexSpot.Color = "#fff9f508"
|
||||
flexSpot.Priority = "3"
|
||||
flexSpot.BackgroundColor = "#ff000000"
|
||||
flexSpot.Comment = flexSpot.Comment + " [New Band]"
|
||||
} else if !spot.NewBand && !spot.NewMode && !spot.NewDXCC && !spot.CallsignWorked && spot.NewSlot {
|
||||
flexSpot.Color = "#ff91d2ff"
|
||||
flexSpot.Priority = "5"
|
||||
flexSpot.BackgroundColor = "#ff000000"
|
||||
flexSpot.Comment = flexSpot.Comment + " [New Slot]"
|
||||
} else if !spot.NewBand && !spot.NewMode && !spot.NewDXCC && !spot.CallsignWorked {
|
||||
flexSpot.Color = "#ffeaeaea"
|
||||
flexSpot.Priority = "5"
|
||||
flexSpot.BackgroundColor = "#ff000000"
|
||||
} else {
|
||||
flexSpot.Color = "#ffeaeaea"
|
||||
flexSpot.Priority = "5"
|
||||
flexSpot.BackgroundColor = "#ff000000"
|
||||
}
|
||||
|
||||
// Send notification to Gotify
|
||||
Gotify(flexSpot)
|
||||
|
||||
flexSpot.Comment = strings.ReplaceAll(flexSpot.Comment, " ", "\u00A0")
|
||||
|
||||
srcFlexSpot, err := fc.Repo.FindDXSameBand(flexSpot)
|
||||
if err != nil {
|
||||
Log.Debugf("Could not find the DX in the database: ", err)
|
||||
}
|
||||
|
||||
var stringSpot string
|
||||
if srcFlexSpot.DX == "" {
|
||||
fc.Repo.CreateSpot(flexSpot)
|
||||
stringSpot = fmt.Sprintf("C%v|spot add rx_freq=%v callsign=%s mode=%s source=%s spotter_callsign=%s timestamp=%v lifetime_seconds=%s comment=%s color=%s background_color=%s priority=%s", flexSpot.CommandNumber, flexSpot.FrequencyMhz,
|
||||
flexSpot.DX, flexSpot.Mode, flexSpot.Source, flexSpot.SpotterCallsign, flexSpot.TimeStamp, flexSpot.LifeTime, flexSpot.Comment, flexSpot.Color, flexSpot.BackgroundColor, flexSpot.Priority)
|
||||
CommandNumber++
|
||||
|
||||
} else if srcFlexSpot.DX != "" && srcFlexSpot.Band == flexSpot.Band {
|
||||
fc.Repo.DeleteSpotByFlexSpotNumber(string(flexSpot.FlexSpotNumber))
|
||||
stringSpot = fmt.Sprintf("C%v|spot remove %v", flexSpot.CommandNumber, srcFlexSpot.FlexSpotNumber)
|
||||
fc.SendSpot(stringSpot)
|
||||
CommandNumber++
|
||||
fc.Repo.CreateSpot(flexSpot)
|
||||
stringSpot = fmt.Sprintf("C%v|spot add rx_freq=%v callsign=%s mode=%s source=%s spotter_callsign=%s timestamp=%v lifetime_seconds=%s comment=%s color=%s background_color=%s priority=%s", flexSpot.CommandNumber, flexSpot.FrequencyMhz,
|
||||
flexSpot.DX, flexSpot.Mode, flexSpot.Source, flexSpot.SpotterCallsign, flexSpot.TimeStamp, flexSpot.LifeTime, flexSpot.Comment, flexSpot.Color, flexSpot.BackgroundColor, flexSpot.Priority)
|
||||
CommandNumber++
|
||||
|
||||
} else if srcFlexSpot.DX != "" && srcFlexSpot.Band != flexSpot.Band {
|
||||
fc.Repo.CreateSpot(flexSpot)
|
||||
stringSpot = fmt.Sprintf("C%v|spot add rx_freq=%v callsign=%s mode=%s source=%s spotter_callsign=%s timestamp=%v lifetime_seconds=%s comment=%s color=%s background_color=%s priority=%s", flexSpot.CommandNumber, flexSpot.FrequencyMhz,
|
||||
flexSpot.DX, flexSpot.Mode, flexSpot.Source, flexSpot.SpotterCallsign, flexSpot.TimeStamp, flexSpot.LifeTime, flexSpot.Comment, flexSpot.Color, flexSpot.BackgroundColor, flexSpot.Priority)
|
||||
CommandNumber++
|
||||
}
|
||||
|
||||
fc.SendSpot(stringSpot)
|
||||
}
|
||||
|
||||
func (fc *FlexClient) SendSpot(stringSpot string) {
|
||||
fc.Write(stringSpot)
|
||||
}
|
||||
|
||||
func (fc *FlexClient) ReadLine() {
|
||||
|
||||
for {
|
||||
message, err := fc.Reader.ReadString(byte('\n'))
|
||||
if err != nil {
|
||||
Log.Errorf("Error reading message from FlexRadio, closing program: %s", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
regRespSpot := *regexp.MustCompile(`R(\d+)\|0\|(\d+)\n`)
|
||||
respSpot := regRespSpot.FindStringSubmatch(message)
|
||||
|
||||
if len(respSpot) > 0 {
|
||||
spot, _ := fc.Repo.FindSpotByCommandNumber(respSpot[1])
|
||||
_, err := fc.Repo.UpdateFlexSpotNumberByID(respSpot[2], *spot)
|
||||
if err != nil {
|
||||
Log.Errorf("Could not update Flex spot number in database: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Response when a spot is clicked
|
||||
regTriggerSpot := *regexp.MustCompile(`.*spot (\d+) triggered.*\n`)
|
||||
respTrigger := regTriggerSpot.FindStringSubmatch(message)
|
||||
|
||||
if len(respTrigger) > 0 {
|
||||
spot, err := fc.Repo.FindSpotByFlexSpotNumber(respTrigger[1])
|
||||
if err != nil {
|
||||
Log.Errorf("could not find spot by flex spot number in database: %s", err)
|
||||
}
|
||||
|
||||
// Sending the callsign to Log4OM
|
||||
SendUDPMessage("<CALLSIGN>" + spot.DX)
|
||||
}
|
||||
|
||||
// Status when a spot is deleted
|
||||
regSpotDeleted := *regexp.MustCompile(`S\d+\|spot (\d+) removed`)
|
||||
respDelete := regSpotDeleted.FindStringSubmatch(message)
|
||||
|
||||
if len(respDelete) > 0 {
|
||||
fc.Repo.DeleteSpotByFlexSpotNumber(respDelete[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write sends raw data to remove telnet server
|
||||
func (fc *FlexClient) Write(data string) (n int, err error) {
|
||||
n, err = fc.Writer.Write([]byte(data + "\n"))
|
||||
if err == nil {
|
||||
err = fc.Writer.Flush()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func DiscoverFlexRadio() (bool, *Discovery) {
|
||||
if Cfg.Flex.Discover {
|
||||
Log.Debugln("FlexRadio Discovery is turned on...searching for radio on the network")
|
||||
|
||||
pc, err := net.ListenPacket("udp", ":4992")
|
||||
if err != nil {
|
||||
Log.Errorf("Could not receive UDP packets to discover FlexRadio: ", err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
|
||||
for {
|
||||
n, _, err := pc.ReadFrom(buf)
|
||||
if err != nil {
|
||||
Log.Errorln("Could not read data on UDP port 4992")
|
||||
}
|
||||
|
||||
discoverRe := regexp.MustCompile(`discovery_protocol_version=.*\smodel=(.*)\sserial=(.*)\sversion=(.*)\snickname=(.*)\scallsign=.*\sip=(.*)\sport=.*`)
|
||||
match := discoverRe.FindStringSubmatch(string(buf[:n]))
|
||||
|
||||
if len(match) > 0 {
|
||||
d := &Discovery{
|
||||
NickName: match[4],
|
||||
Model: match[1],
|
||||
Serial: match[2],
|
||||
Version: match[3],
|
||||
IP: match[5],
|
||||
}
|
||||
return true, d
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.Infoln("FlexRadio Discovery is turned off...using IP provided in the config file")
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
24
go.mod
Normal file
24
go.mod
Normal file
@@ -0,0 +1,24 @@
|
||||
module git.rouggy.com/rouggy/FlexDXCluster
|
||||
|
||||
go 1.23.1
|
||||
|
||||
require (
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/mattn/go-sqlite3 v1.14.23
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/gorilla/mux v1.8.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/gomega v1.35.1 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/term v0.24.0 // indirect
|
||||
)
|
117
go.sum
Normal file
117
go.sum
Normal file
@@ -0,0 +1,117 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
||||
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/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=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
89
gotify.go
Normal file
89
gotify.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GotifyMessage struct {
|
||||
Title string `json:"title"`
|
||||
Message string `json:"message"`
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
|
||||
func Gotify(spot FlexSpot) {
|
||||
|
||||
ExceptionList := "4U1UN 5Z4B OH2B CS3B"
|
||||
|
||||
if Cfg.Gotify.Enable && !strings.Contains(ExceptionList, spot.DX) {
|
||||
|
||||
message := fmt.Sprintf("DX: %s\nFrom: %s\nFreq: %s\nMode: %s\nCountry: %s\nTime: %s\n", spot.DX, spot.SpotterCallsign, spot.FrequencyMhz, spot.Mode, spot.CountryName, spot.UTCTime)
|
||||
|
||||
gotifyMsg := GotifyMessage{
|
||||
Title: "",
|
||||
Message: message,
|
||||
Priority: 10,
|
||||
}
|
||||
|
||||
if spot.NewDXCC && Cfg.Gotify.NewDXCC {
|
||||
title := "FlexDXCluster New DXCC"
|
||||
gotifyMsg.Title = title
|
||||
gotifyMsg.Message = message
|
||||
sendToGotify(gotifyMsg)
|
||||
}
|
||||
|
||||
if spot.NewBand && spot.NewMode && Cfg.Gotify.NewBandAndMode {
|
||||
title := "FlexDXCluster New Mode & Band"
|
||||
gotifyMsg.Title = title
|
||||
gotifyMsg.Message = message
|
||||
sendToGotify(gotifyMsg)
|
||||
}
|
||||
|
||||
if spot.NewMode && Cfg.Gotify.NewMode && !spot.NewBand {
|
||||
title := "FlexDXCluster New Mode"
|
||||
gotifyMsg.Title = title
|
||||
gotifyMsg.Message = message
|
||||
sendToGotify(gotifyMsg)
|
||||
}
|
||||
|
||||
if spot.NewBand && Cfg.Gotify.NewBand && !spot.NewMode {
|
||||
title := "FlexDXCluster New Band"
|
||||
gotifyMsg.Title = title
|
||||
gotifyMsg.Message = message
|
||||
sendToGotify(gotifyMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendToGotify(mess GotifyMessage) {
|
||||
jsonData, err := json.Marshal(mess)
|
||||
if err != nil {
|
||||
Log.Errorln("Error marshaling JSON:", err)
|
||||
return
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", Cfg.Gotify.URL, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
Log.Errorln("Error creating request:", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Add("Authorization", "Bearer "+Cfg.Gotify.Token)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
Log.Errorln("Error sending request:", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
Log.Errorln("Gotify server returned non-OK status:", resp.Status)
|
||||
} else {
|
||||
Log.Debugln("Push successfully sent to Gotify")
|
||||
}
|
||||
}
|
298
httpserver.go
Normal file
298
httpserver.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type HTTPServer struct {
|
||||
Router *mux.Router
|
||||
FlexRepo *FlexDXClusterRepository
|
||||
ContactRepo *Log4OMContactsRepository
|
||||
TCPServer *TCPServer
|
||||
TCPClient *TCPClient
|
||||
FlexClient *FlexClient
|
||||
Port string
|
||||
Log *log.Logger
|
||||
statsCache Stats
|
||||
statsMutex sync.RWMutex
|
||||
lastUpdate time.Time
|
||||
}
|
||||
|
||||
type Stats struct {
|
||||
TotalSpots int `json:"totalSpots"`
|
||||
ActiveSpotters int `json:"activeSpotters"`
|
||||
NewDXCC int `json:"newDXCC"`
|
||||
ConnectedClients int `json:"connectedClients"`
|
||||
TotalContacts int `json:"totalContacts"`
|
||||
ClusterStatus string `json:"clusterStatus"`
|
||||
FlexStatus string `json:"flexStatus"`
|
||||
Filters Filters `json:"filters"`
|
||||
}
|
||||
|
||||
type Filters struct {
|
||||
Skimmer bool `json:"skimmer"`
|
||||
FT8 bool `json:"ft8"`
|
||||
FT4 bool `json:"ft4"`
|
||||
}
|
||||
|
||||
type APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func NewHTTPServer(flexRepo *FlexDXClusterRepository, contactRepo *Log4OMContactsRepository,
|
||||
tcpServer *TCPServer, tcpClient *TCPClient, flexClient *FlexClient, port string) *HTTPServer {
|
||||
|
||||
server := &HTTPServer{
|
||||
Router: mux.NewRouter(),
|
||||
FlexRepo: flexRepo,
|
||||
ContactRepo: contactRepo,
|
||||
TCPServer: tcpServer,
|
||||
TCPClient: tcpClient,
|
||||
FlexClient: flexClient,
|
||||
Port: port,
|
||||
Log: Log,
|
||||
}
|
||||
|
||||
server.setupRoutes()
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *HTTPServer) setupRoutes() {
|
||||
// Enable CORS
|
||||
s.Router.Use(s.corsMiddleware)
|
||||
|
||||
// API Routes
|
||||
api := s.Router.PathPrefix("/api").Subrouter()
|
||||
|
||||
api.HandleFunc("/stats", s.getStats).Methods("GET", "OPTIONS")
|
||||
api.HandleFunc("/spots", s.getSpots).Methods("GET", "OPTIONS")
|
||||
api.HandleFunc("/spots/{id}", s.getSpotByID).Methods("GET", "OPTIONS")
|
||||
api.HandleFunc("/spotters", s.getTopSpotters).Methods("GET", "OPTIONS")
|
||||
api.HandleFunc("/contacts", s.getContacts).Methods("GET", "OPTIONS")
|
||||
api.HandleFunc("/send-command", s.sendCommand).Methods("POST", "OPTIONS")
|
||||
api.HandleFunc("/filters", s.updateFilters).Methods("POST", "OPTIONS")
|
||||
api.HandleFunc("/shutdown", s.shutdownApp).Methods("POST", "OPTIONS")
|
||||
|
||||
// Serve static files (dashboard)
|
||||
s.Router.PathPrefix("/").Handler(http.FileServer(http.Dir("./static")))
|
||||
}
|
||||
|
||||
func (s *HTTPServer) corsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *HTTPServer) getStats(w http.ResponseWriter, r *http.Request) {
|
||||
// Cache stats for 5 seconds to avoid overwhelming the database
|
||||
s.statsMutex.RLock()
|
||||
if time.Since(s.lastUpdate) < 5*time.Second {
|
||||
stats := s.statsCache
|
||||
s.statsMutex.RUnlock()
|
||||
s.sendJSON(w, APIResponse{Success: true, Data: stats})
|
||||
return
|
||||
}
|
||||
s.statsMutex.RUnlock()
|
||||
|
||||
// Calculate fresh stats
|
||||
s.statsMutex.Lock()
|
||||
defer s.statsMutex.Unlock()
|
||||
|
||||
allSpots := s.FlexRepo.GetAllSpots("0")
|
||||
spotters := s.FlexRepo.GetSpotters()
|
||||
contacts := s.ContactRepo.CountEntries()
|
||||
|
||||
newDXCCCount := 0
|
||||
for _, spot := range allSpots {
|
||||
if spot.NewDXCC {
|
||||
newDXCCCount++
|
||||
}
|
||||
}
|
||||
|
||||
clusterStatus := "disconnected"
|
||||
if s.TCPClient != nil && s.TCPClient.LoggedIn {
|
||||
clusterStatus = "connected"
|
||||
}
|
||||
|
||||
flexStatus := "disconnected"
|
||||
if s.FlexClient != nil && s.FlexClient.IsConnected {
|
||||
flexStatus = "connected"
|
||||
}
|
||||
|
||||
s.statsCache = Stats{
|
||||
TotalSpots: len(allSpots),
|
||||
ActiveSpotters: len(spotters),
|
||||
NewDXCC: newDXCCCount,
|
||||
ConnectedClients: len(s.TCPServer.Clients),
|
||||
TotalContacts: contacts,
|
||||
ClusterStatus: clusterStatus,
|
||||
FlexStatus: flexStatus,
|
||||
Filters: Filters{
|
||||
Skimmer: Cfg.Cluster.Skimmer,
|
||||
FT8: Cfg.Cluster.FT8,
|
||||
FT4: Cfg.Cluster.FT4,
|
||||
},
|
||||
}
|
||||
s.lastUpdate = time.Now()
|
||||
|
||||
s.sendJSON(w, APIResponse{Success: true, Data: s.statsCache})
|
||||
}
|
||||
|
||||
func (s *HTTPServer) getSpots(w http.ResponseWriter, r *http.Request) {
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
if limitStr == "" {
|
||||
limitStr = "50"
|
||||
}
|
||||
|
||||
spots := s.FlexRepo.GetAllSpots(limitStr)
|
||||
|
||||
s.sendJSON(w, APIResponse{Success: true, Data: spots})
|
||||
}
|
||||
|
||||
func (s *HTTPServer) getSpotByID(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
spot, err := s.FlexRepo.FindSpotByFlexSpotNumber(id)
|
||||
if err != nil {
|
||||
s.sendJSON(w, APIResponse{Success: false, Error: "Spot not found"})
|
||||
return
|
||||
}
|
||||
|
||||
s.sendJSON(w, APIResponse{Success: true, Data: spot})
|
||||
}
|
||||
|
||||
func (s *HTTPServer) getTopSpotters(w http.ResponseWriter, r *http.Request) {
|
||||
spotters := s.FlexRepo.GetSpotters()
|
||||
s.sendJSON(w, APIResponse{Success: true, Data: spotters})
|
||||
}
|
||||
|
||||
func (s *HTTPServer) getContacts(w http.ResponseWriter, r *http.Request) {
|
||||
count := s.ContactRepo.CountEntries()
|
||||
|
||||
data := map[string]interface{}{
|
||||
"totalContacts": count,
|
||||
}
|
||||
|
||||
s.sendJSON(w, APIResponse{Success: true, Data: data})
|
||||
}
|
||||
|
||||
type CommandRequest struct {
|
||||
Command string `json:"command"`
|
||||
}
|
||||
|
||||
func (s *HTTPServer) sendCommand(w http.ResponseWriter, r *http.Request) {
|
||||
var req CommandRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
s.sendJSON(w, APIResponse{Success: false, Error: "Invalid request"})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Command == "" {
|
||||
s.sendJSON(w, APIResponse{Success: false, Error: "Command is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Send command to cluster via TCPClient
|
||||
s.TCPClient.CmdChan <- req.Command
|
||||
|
||||
s.Log.Infof("Command sent via HTTP API: %s", req.Command)
|
||||
s.sendJSON(w, APIResponse{Success: true, Data: map[string]string{"message": "Command sent successfully"}})
|
||||
}
|
||||
|
||||
type FilterRequest struct {
|
||||
Skimmer *bool `json:"skimmer,omitempty"`
|
||||
FT8 *bool `json:"ft8,omitempty"`
|
||||
FT4 *bool `json:"ft4,omitempty"`
|
||||
}
|
||||
|
||||
func (s *HTTPServer) updateFilters(w http.ResponseWriter, r *http.Request) {
|
||||
var req FilterRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
s.sendJSON(w, APIResponse{Success: false, Error: "Invalid request"})
|
||||
return
|
||||
}
|
||||
|
||||
commands := []string{}
|
||||
|
||||
if req.Skimmer != nil {
|
||||
if *req.Skimmer {
|
||||
commands = append(commands, "set/skimmer")
|
||||
Cfg.Cluster.Skimmer = true
|
||||
} else {
|
||||
commands = append(commands, "set/noskimmer")
|
||||
Cfg.Cluster.Skimmer = false
|
||||
}
|
||||
}
|
||||
|
||||
if req.FT8 != nil {
|
||||
if *req.FT8 {
|
||||
commands = append(commands, "set/ft8")
|
||||
Cfg.Cluster.FT8 = true
|
||||
} else {
|
||||
commands = append(commands, "set/noft8")
|
||||
Cfg.Cluster.FT8 = false
|
||||
}
|
||||
}
|
||||
|
||||
if req.FT4 != nil {
|
||||
if *req.FT4 {
|
||||
commands = append(commands, "set/ft4")
|
||||
Cfg.Cluster.FT4 = true
|
||||
} else {
|
||||
commands = append(commands, "set/noft4")
|
||||
Cfg.Cluster.FT4 = false
|
||||
}
|
||||
}
|
||||
|
||||
for _, cmd := range commands {
|
||||
s.TCPClient.CmdChan <- cmd
|
||||
}
|
||||
|
||||
s.sendJSON(w, APIResponse{Success: true, Data: map[string]string{"message": "Filters updated successfully"}})
|
||||
}
|
||||
|
||||
func (s *HTTPServer) sendJSON(w http.ResponseWriter, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func (s *HTTPServer) shutdownApp(w http.ResponseWriter, r *http.Request) {
|
||||
s.Log.Info("Shutdown request received from dashboard")
|
||||
|
||||
s.sendJSON(w, APIResponse{Success: true, Data: map[string]string{"message": "Shutting down FlexDXCluster"}})
|
||||
|
||||
// Shutdown gracefully after a short delay to allow response to be sent
|
||||
go func() {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
s.Log.Info("Initiating shutdown...")
|
||||
os.Exit(0)
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *HTTPServer) Start() {
|
||||
s.Log.Infof("HTTP Server starting on port %s", s.Port)
|
||||
s.Log.Infof("Dashboard available at http://localhost:%s", s.Port)
|
||||
|
||||
if err := http.ListenAndServe(":"+s.Port, s.Router); err != nil {
|
||||
s.Log.Fatalf("Failed to start HTTP server: %v", err)
|
||||
}
|
||||
}
|
BIN
images/FlexDXCluster.ico
Normal file
BIN
images/FlexDXCluster.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 202 KiB |
BIN
images/background.jpg
Normal file
BIN
images/background.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 MiB |
72
log.go
Normal file
72
log.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
prefixed "github.com/x-cray/logrus-prefixed-formatter"
|
||||
)
|
||||
|
||||
var Log *log.Logger
|
||||
|
||||
func NewLog() *log.Logger {
|
||||
|
||||
if Cfg.General.DeleteLogFileAtStart {
|
||||
if _, err := os.Stat("flexradio.log"); err == nil {
|
||||
os.Remove("flexradio.log")
|
||||
}
|
||||
}
|
||||
|
||||
var w io.Writer
|
||||
if Cfg.General.LogToFile {
|
||||
f, err := os.OpenFile("flexradio.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
w = io.MultiWriter(os.Stdout, f)
|
||||
} else {
|
||||
w = io.Writer(os.Stdout)
|
||||
}
|
||||
|
||||
Log = &log.Logger{
|
||||
Out: w,
|
||||
Formatter: &prefixed.TextFormatter{
|
||||
DisableColors: false,
|
||||
TimestampFormat: "02-01-2006 15:04:05",
|
||||
FullTimestamp: true,
|
||||
ForceFormatting: true,
|
||||
},
|
||||
}
|
||||
|
||||
if Cfg.General.LogLevel == "DEBUG" {
|
||||
Log.Level = log.DebugLevel
|
||||
} else if Cfg.General.LogLevel == "INFO" {
|
||||
Log.Level = log.InfoLevel
|
||||
} else if Cfg.General.LogLevel == "WARN" {
|
||||
Log.Level = log.WarnLevel
|
||||
} else {
|
||||
Log.Level = log.InfoLevel
|
||||
}
|
||||
|
||||
return Log
|
||||
}
|
||||
|
||||
// Info ...
|
||||
// func Info(format string, v ...interface{}) {
|
||||
// log.Infof(format, v...)
|
||||
// }
|
||||
|
||||
// // Warn ...
|
||||
// func Warn(format string, v ...interface{}) {
|
||||
// log.Warnf(format, v...)
|
||||
// }
|
||||
|
||||
// // Error ...
|
||||
// func Error(format string, v ...interface{}) {
|
||||
// log.Errorf(format, v...)
|
||||
// }
|
||||
|
||||
// func Debug(format string, v ...interface{}) {
|
||||
// log.Debugf(format, v...)
|
||||
// }
|
91
main.go
Normal file
91
main.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var Mutex sync.Mutex
|
||||
|
||||
func ParseFlags() (string, error) {
|
||||
// String that contains the configured configuration path
|
||||
var configPath string
|
||||
|
||||
exe, _ := os.Executable()
|
||||
defaultCfgPath := filepath.Dir(exe)
|
||||
defaultCfgPath = filepath.Join(defaultCfgPath, "/config.yml")
|
||||
// Set up a CLI flag called "-config" to allow users
|
||||
// to supply the configuration file
|
||||
flag.StringVar(&configPath, "config", defaultCfgPath, "path to config file")
|
||||
|
||||
// Actually parse the flags
|
||||
flag.Parse()
|
||||
|
||||
// Validate the path first
|
||||
if err := ValidateConfigPath(configPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Return the configuration path
|
||||
return configPath, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
// Generate our config based on the config supplied
|
||||
// by the user in the flags
|
||||
cfgPath, err := ParseFlags()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cfg := NewConfig(cfgPath)
|
||||
|
||||
log := NewLog()
|
||||
log.Info("Running FlexDXCluster version 1.0")
|
||||
log.Infof("Callsign: %s", cfg.General.Callsign)
|
||||
|
||||
DeleteDatabase("./flex.sqlite", log)
|
||||
|
||||
log.Debugf("Gotify Push Enabled: %v", cfg.Gotify.Enable)
|
||||
if cfg.Gotify.Enable {
|
||||
log.Debugf("Gotify Push NewDXCC: %v - NewBand: %v - NewMode: %v - NewBandAndMode: %v", cfg.Gotify.NewDXCC, cfg.Gotify.NewBand, cfg.Gotify.NewMode, cfg.Gotify.NewBandAndMode)
|
||||
}
|
||||
|
||||
// Load country.xml to get all the DXCC number
|
||||
Countries := LoadCountryFile()
|
||||
log.Debug("XML Country File has been loaded properly.")
|
||||
|
||||
// Database to keep track of all spots
|
||||
fRepo := NewFlexDXDatabase("flex.sqlite")
|
||||
defer fRepo.db.Close()
|
||||
|
||||
// Database connection to Log4OM
|
||||
cRepo := NewLog4OMContactsRepository(cfg.SQLite.SQLitePath)
|
||||
contacts := cRepo.CountEntries()
|
||||
log.Infof("Log4OM Database Contains %v Contacts", contacts)
|
||||
defer cRepo.db.Close()
|
||||
|
||||
// Initialize servers and clients
|
||||
TCPServer := NewTCPServer(cfg.TelnetServer.Host, cfg.TelnetServer.Port)
|
||||
TCPClient := NewTCPClient(TCPServer, Countries)
|
||||
FlexClient := NewFlexClient(*fRepo, TCPServer, TCPClient.SpotChanToFlex)
|
||||
|
||||
// Initialize HTTP Server for Dashboard
|
||||
HTTPServer := NewHTTPServer(fRepo, cRepo, TCPServer, TCPClient, FlexClient, "8080")
|
||||
|
||||
// Start all services
|
||||
go FlexClient.StartFlexClient()
|
||||
go TCPClient.StartClient()
|
||||
go TCPServer.StartServer()
|
||||
go HTTPServer.Start()
|
||||
|
||||
log.Infof("Telnet Server: %s:%s", cfg.TelnetServer.Host, cfg.TelnetServer.Port)
|
||||
log.Infof("Cluster: %s:%s", cfg.Cluster.Server, cfg.Cluster.Port)
|
||||
|
||||
CheckSignal(TCPClient, TCPServer, FlexClient, fRepo, cRepo)
|
||||
|
||||
}
|
BIN
rsrc_windows_amd64.syso
Normal file
BIN
rsrc_windows_amd64.syso
Normal file
Binary file not shown.
395
spot.go
Normal file
395
spot.go
Normal file
@@ -0,0 +1,395 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
type TelnetSpot struct {
|
||||
DX string
|
||||
Spotter string
|
||||
Frequency string
|
||||
Mode string
|
||||
Band string
|
||||
Time string
|
||||
DXCC string
|
||||
CountryName string
|
||||
Comment string
|
||||
CommandNumber int
|
||||
FlexSpotNumber int
|
||||
NewDXCC bool
|
||||
NewBand bool
|
||||
NewMode bool
|
||||
NewSlot bool
|
||||
CallsignWorked bool
|
||||
}
|
||||
|
||||
// var spotNumber = 1
|
||||
|
||||
func ProcessTelnetSpot(re *regexp.Regexp, spotRaw string, SpotChanToFlex chan TelnetSpot, SpotChanToHTTPServer chan TelnetSpot, Countries Countries) {
|
||||
|
||||
match := re.FindStringSubmatch(spotRaw)
|
||||
|
||||
if len(match) != 0 {
|
||||
spot := TelnetSpot{
|
||||
DX: match[3],
|
||||
Spotter: match[1],
|
||||
Frequency: match[2],
|
||||
Mode: match[4],
|
||||
Comment: strings.Trim(match[5], " "),
|
||||
Time: match[6],
|
||||
}
|
||||
|
||||
DXCC := GetDXCC(spot.DX, Countries)
|
||||
spot.DXCC = DXCC.DXCC
|
||||
spot.CountryName = DXCC.CountryName
|
||||
|
||||
if spot.DXCC == "" {
|
||||
Log.Errorf("Could not identify the DXCC for %s", spot.DX)
|
||||
return
|
||||
}
|
||||
|
||||
spot.GetBand()
|
||||
spot.GuessMode()
|
||||
spot.CallsignWorked = false
|
||||
spot.NewBand = false
|
||||
spot.NewMode = false
|
||||
spot.NewDXCC = false
|
||||
spot.NewSlot = false
|
||||
|
||||
contactRepo := NewLog4OMContactsRepository(Cfg.SQLite.SQLitePath)
|
||||
defer contactRepo.db.Close()
|
||||
|
||||
contactsChan := make(chan []Contact)
|
||||
contactsModeChan := make(chan []Contact)
|
||||
contactsModeBandChan := make(chan []Contact)
|
||||
contactsBandChan := make(chan []Contact)
|
||||
contactsCallChan := make(chan []Contact)
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
wg.Add(5)
|
||||
|
||||
go contactRepo.ListByCountry(spot.DXCC, contactsChan, wg)
|
||||
contacts := <-contactsChan
|
||||
|
||||
go contactRepo.ListByCountryMode(spot.DXCC, spot.Mode, contactsModeChan, wg)
|
||||
contactsMode := <-contactsModeChan
|
||||
|
||||
go contactRepo.ListByCountryBand(spot.DXCC, spot.Band, contactsBandChan, wg)
|
||||
contactsBand := <-contactsBandChan
|
||||
|
||||
go contactRepo.ListByCallSign(spot.DX, spot.Band, spot.Mode, contactsCallChan, wg)
|
||||
contactsCall := <-contactsCallChan
|
||||
|
||||
go contactRepo.ListByCountryModeBand(spot.DXCC, spot.Band, spot.Mode, contactsModeBandChan, wg)
|
||||
contactsModeBand := <-contactsModeBandChan
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if len(contacts) == 0 {
|
||||
spot.NewDXCC = true
|
||||
}
|
||||
|
||||
if len(contactsMode) == 0 {
|
||||
spot.NewMode = true
|
||||
}
|
||||
if len(contactsBand) == 0 {
|
||||
spot.NewBand = true
|
||||
}
|
||||
|
||||
if len(contactsModeBand) == 0 && !spot.NewDXCC && !spot.NewBand && !spot.NewMode {
|
||||
spot.NewSlot = true
|
||||
}
|
||||
|
||||
if len(contactsCall) > 0 {
|
||||
spot.CallsignWorked = true
|
||||
}
|
||||
|
||||
// Send spots to FlexRadio
|
||||
SpotChanToFlex <- spot
|
||||
|
||||
if spot.NewDXCC {
|
||||
Log.Debugf("(** New DXCC **) DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s",
|
||||
spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC)
|
||||
}
|
||||
|
||||
if !spot.NewDXCC && spot.NewBand && spot.NewMode {
|
||||
Log.Debugf("(** New Band/Mode **) DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s",
|
||||
spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC)
|
||||
}
|
||||
|
||||
if !spot.NewDXCC && spot.NewBand && !spot.NewMode {
|
||||
Log.Debugf("(** New Band **) DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s",
|
||||
spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC)
|
||||
}
|
||||
|
||||
if !spot.NewDXCC && !spot.NewBand && spot.NewMode && spot.Mode != "" {
|
||||
Log.Debugf("(** New Mode **) DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s",
|
||||
spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC)
|
||||
}
|
||||
|
||||
if !spot.NewDXCC && !spot.NewBand && !spot.NewMode && spot.NewSlot && spot.Mode != "" {
|
||||
Log.Debugf("(** New Slot **) DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s",
|
||||
spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC)
|
||||
}
|
||||
|
||||
if !spot.NewDXCC && !spot.NewBand && !spot.NewMode && spot.CallsignWorked {
|
||||
Log.Debugf("(** Worked **) DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s",
|
||||
spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC)
|
||||
}
|
||||
|
||||
if !spot.NewDXCC && !spot.NewBand && !spot.NewMode && !spot.CallsignWorked {
|
||||
Log.Debugf("DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s",
|
||||
spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC)
|
||||
}
|
||||
} else {
|
||||
// Log.Infof("Could not decode: %s", strings.Trim(spotRaw, "\n"))
|
||||
}
|
||||
|
||||
// Log.Infof("Spots Processed: %v", spotNumber)
|
||||
// spotNumber++
|
||||
}
|
||||
|
||||
func (spot *TelnetSpot) GetBand() {
|
||||
freq := FreqMhztoHz(spot.Frequency)
|
||||
switch true {
|
||||
case strings.HasPrefix(freq, "1.8"):
|
||||
spot.Band = "160M"
|
||||
if spot.Mode == "SSB" {
|
||||
spot.Mode = "LSB"
|
||||
}
|
||||
case strings.HasPrefix(freq, "3."):
|
||||
spot.Band = "80M"
|
||||
if spot.Mode == "SSB" {
|
||||
spot.Mode = "LSB"
|
||||
}
|
||||
case strings.HasPrefix(freq, "5."):
|
||||
spot.Band = "60M"
|
||||
if spot.Mode == "SSB" {
|
||||
spot.Mode = "LSB"
|
||||
}
|
||||
case strings.HasPrefix(freq, "7."):
|
||||
spot.Band = "40M"
|
||||
if spot.Mode == "SSB" {
|
||||
spot.Mode = "LSB"
|
||||
}
|
||||
case strings.HasPrefix(freq, "10."):
|
||||
spot.Band = "30M"
|
||||
case strings.HasPrefix(freq, "14."):
|
||||
spot.Band = "20M"
|
||||
if spot.Mode == "SSB" {
|
||||
spot.Mode = "USB"
|
||||
}
|
||||
case strings.HasPrefix(freq, "18."):
|
||||
spot.Band = "17M"
|
||||
if spot.Mode == "SSB" {
|
||||
spot.Mode = "USB"
|
||||
}
|
||||
case strings.HasPrefix(freq, "21."):
|
||||
spot.Band = "15M"
|
||||
if spot.Mode == "SSB" {
|
||||
spot.Mode = "USB"
|
||||
}
|
||||
case strings.HasPrefix(freq, "24."):
|
||||
spot.Band = "12M"
|
||||
if spot.Mode == "SSB" {
|
||||
spot.Mode = "USB"
|
||||
}
|
||||
case strings.HasPrefix(freq, "28."):
|
||||
spot.Band = "10M"
|
||||
if spot.Mode == "SSB" {
|
||||
spot.Mode = "USB"
|
||||
}
|
||||
case strings.HasPrefix(freq, "29."):
|
||||
spot.Band = "10M"
|
||||
if spot.Mode == "SSB" {
|
||||
spot.Mode = "USB"
|
||||
}
|
||||
case strings.HasPrefix(freq, "50."):
|
||||
spot.Band = "6M"
|
||||
if spot.Mode == "SSB" {
|
||||
spot.Mode = "USB"
|
||||
}
|
||||
default:
|
||||
spot.Band = "N/A"
|
||||
}
|
||||
}
|
||||
|
||||
func (spot *TelnetSpot) GuessMode() {
|
||||
if spot.Mode == "" {
|
||||
freqInt, err := strconv.ParseFloat(spot.Frequency, 32)
|
||||
if err != nil {
|
||||
Log.Errorf("could not convert frequency string in float64:", err)
|
||||
}
|
||||
|
||||
switch spot.Band {
|
||||
case "160M":
|
||||
if freqInt >= 1800 && freqInt <= 1840 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 1840 && freqInt <= 1844 {
|
||||
spot.Mode = "FT8"
|
||||
}
|
||||
case "80M":
|
||||
if freqInt >= 3500 && freqInt < 3568 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 3568 && freqInt < 3573 {
|
||||
spot.Mode = "FT4"
|
||||
}
|
||||
if freqInt >= 3573 && freqInt < 3580 {
|
||||
spot.Mode = "FT8"
|
||||
}
|
||||
if freqInt >= 3580 && freqInt < 3600 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 3600 && freqInt <= 3800 {
|
||||
spot.Mode = "LSB"
|
||||
}
|
||||
case "60M":
|
||||
if freqInt >= 5351.5 && freqInt < 5354 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 5354 && freqInt < 5366 {
|
||||
spot.Mode = "LSB"
|
||||
}
|
||||
if freqInt >= 5366 && freqInt <= 5266.5 {
|
||||
spot.Mode = "FT8"
|
||||
}
|
||||
case "40M":
|
||||
if freqInt >= 7000 && freqInt < 7045.5 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 7045.5 && freqInt < 7048.5 {
|
||||
spot.Mode = "FT4"
|
||||
}
|
||||
if freqInt >= 7048.5 && freqInt < 7074 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 7074 && freqInt < 7078 {
|
||||
spot.Mode = "FT8"
|
||||
}
|
||||
if freqInt >= 7078 && freqInt <= 7300 {
|
||||
spot.Mode = "LSB"
|
||||
}
|
||||
case "30M":
|
||||
if freqInt >= 10100 && freqInt < 10130 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 10130 && freqInt < 10140 {
|
||||
spot.Mode = "FT8"
|
||||
}
|
||||
if freqInt >= 10140 && freqInt <= 10150 {
|
||||
spot.Mode = "FT4"
|
||||
}
|
||||
case "20M":
|
||||
if freqInt >= 14000 && freqInt < 14074 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 14074 && freqInt < 14078 {
|
||||
spot.Mode = "FT8"
|
||||
}
|
||||
if freqInt >= 14074 && freqInt < 14078 {
|
||||
spot.Mode = "FT8"
|
||||
}
|
||||
if freqInt >= 14078 && freqInt < 14083 {
|
||||
spot.Mode = "FT4"
|
||||
}
|
||||
if freqInt >= 14083 && freqInt < 14119 {
|
||||
spot.Mode = "FT8"
|
||||
}
|
||||
if freqInt >= 14119 && freqInt < 14350 {
|
||||
spot.Mode = "USB"
|
||||
}
|
||||
|
||||
case "17M":
|
||||
if freqInt >= 18068 && freqInt < 18095 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 18095 && freqInt < 18104 {
|
||||
spot.Mode = "FT8"
|
||||
}
|
||||
if freqInt >= 18104 && freqInt < 18108 {
|
||||
spot.Mode = "FT4"
|
||||
}
|
||||
if freqInt >= 18108 && freqInt <= 18168 {
|
||||
spot.Mode = "USB"
|
||||
}
|
||||
|
||||
case "15M":
|
||||
if freqInt >= 21000 && freqInt < 21074 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 21074 && freqInt < 21100 {
|
||||
spot.Mode = "FT8"
|
||||
}
|
||||
if freqInt >= 21100 && freqInt < 21140 {
|
||||
spot.Mode = "RTTY"
|
||||
}
|
||||
if freqInt >= 21140 && freqInt < 21144 {
|
||||
spot.Mode = "FT4"
|
||||
}
|
||||
if freqInt >= 21144 && freqInt <= 21450 {
|
||||
spot.Mode = "USB"
|
||||
}
|
||||
|
||||
case "12M":
|
||||
if freqInt >= 24890 && freqInt < 24915 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 24915 && freqInt < 24919 {
|
||||
spot.Mode = "FT8"
|
||||
}
|
||||
if freqInt >= 24919 && freqInt < 24930 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 24930 && freqInt <= 24990 {
|
||||
spot.Mode = "USB"
|
||||
}
|
||||
|
||||
case "10M":
|
||||
if freqInt >= 28000 && freqInt < 28074 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 28074 && freqInt < 28080 {
|
||||
spot.Mode = "FT8"
|
||||
}
|
||||
if freqInt >= 28080 && freqInt < 28100 {
|
||||
spot.Mode = "RTTY"
|
||||
}
|
||||
if freqInt >= 28100 && freqInt < 28300 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 28300 && freqInt < 29000 {
|
||||
spot.Mode = "USB"
|
||||
}
|
||||
if freqInt >= 29000 && freqInt <= 29700 {
|
||||
spot.Mode = "FM"
|
||||
}
|
||||
case "6M":
|
||||
if freqInt >= 50000 && freqInt < 50100 {
|
||||
spot.Mode = "CW"
|
||||
}
|
||||
if freqInt >= 50100 && freqInt < 50313 {
|
||||
spot.Mode = "USB"
|
||||
}
|
||||
if freqInt >= 50313 && freqInt < 50320 {
|
||||
spot.Mode = "FT8"
|
||||
}
|
||||
if freqInt >= 50320 && freqInt < 50400 {
|
||||
spot.Mode = "USB"
|
||||
}
|
||||
if freqInt >= 50400 && freqInt < +52000 {
|
||||
spot.Mode = "FM"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if spot.Mode == "" {
|
||||
Log.Errorf("Could not identify mode for %s on %s", spot.DX, spot.Frequency)
|
||||
}
|
||||
}
|
582
static/index.html
Normal file
582
static/index.html
Normal file
@@ -0,0 +1,582 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>FlexDXCluster Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
}
|
||||
.animate-spin { animation: spin 1s linear infinite; }
|
||||
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||
.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
||||
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: .5; } }
|
||||
.full-height { height: 100vh; display: flex; flex-direction: column; }
|
||||
.scrollable { overflow-y: auto; flex: 1; }
|
||||
::-webkit-scrollbar { width: 8px; }
|
||||
::-webkit-scrollbar-track { background: rgb(15 23 42); }
|
||||
::-webkit-scrollbar-thumb { background: rgb(51 65 85); border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgb(71 85 105); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 text-white">
|
||||
<div id="app" class="full-height p-4"></div>
|
||||
|
||||
<script>
|
||||
const API_BASE_URL = 'http://localhost:8080/api';
|
||||
|
||||
let state = {
|
||||
spots: [],
|
||||
filteredSpots: [],
|
||||
stats: {
|
||||
totalSpots: 0,
|
||||
activeSpotters: 0,
|
||||
newDXCC: 0,
|
||||
connectedClients: 0,
|
||||
totalContacts: 0,
|
||||
clusterStatus: 'disconnected',
|
||||
flexStatus: 'disconnected',
|
||||
filters: { skimmer: false, ft8: false, ft4: false }
|
||||
},
|
||||
topSpotters: [],
|
||||
loading: true,
|
||||
command: '',
|
||||
spotFilters: {
|
||||
showAll: true,
|
||||
showNewDXCC: false,
|
||||
showNewBand: false,
|
||||
showNewMode: false,
|
||||
showNewBandMode: false,
|
||||
showNewSlot: false,
|
||||
showWorked: false,
|
||||
band160M: false,
|
||||
band80M: false,
|
||||
band60M: false,
|
||||
band40M: false,
|
||||
band30M: false,
|
||||
band20M: false,
|
||||
band17M: false,
|
||||
band15M: false,
|
||||
band12M: false,
|
||||
band10M: false,
|
||||
band6M: false
|
||||
}
|
||||
};
|
||||
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [statsRes, spotsRes, spottersRes] = await Promise.all([
|
||||
fetch(`${API_BASE_URL}/stats`),
|
||||
fetch(`${API_BASE_URL}/spots?limit=100`),
|
||||
fetch(`${API_BASE_URL}/spotters`)
|
||||
]);
|
||||
|
||||
const statsData = await statsRes.json();
|
||||
const spotsData = await spotsRes.json();
|
||||
const spottersData = await spottersRes.json();
|
||||
|
||||
if (statsData.success) state.stats = statsData.data;
|
||||
if (spotsData.success) {
|
||||
state.spots = spotsData.data || [];
|
||||
applySpotFilters();
|
||||
}
|
||||
if (spottersData.success) state.topSpotters = spottersData.data || [];
|
||||
|
||||
state.loading = false;
|
||||
render();
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
state.loading = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function applySpotFilters() {
|
||||
if (state.spotFilters.showAll) {
|
||||
state.filteredSpots = state.spots;
|
||||
return;
|
||||
}
|
||||
|
||||
const bandFiltersActive = state.spotFilters.band160M || state.spotFilters.band80M ||
|
||||
state.spotFilters.band60M || state.spotFilters.band40M || state.spotFilters.band30M ||
|
||||
state.spotFilters.band20M || state.spotFilters.band17M || state.spotFilters.band15M ||
|
||||
state.spotFilters.band12M || state.spotFilters.band10M || state.spotFilters.band6M;
|
||||
|
||||
const typeFiltersActive = state.spotFilters.showNewDXCC || state.spotFilters.showNewBand ||
|
||||
state.spotFilters.showNewMode || state.spotFilters.showNewBandMode ||
|
||||
state.spotFilters.showNewSlot || state.spotFilters.showWorked;
|
||||
|
||||
state.filteredSpots = state.spots.filter(spot => {
|
||||
let matchesBand = false;
|
||||
let matchesType = false;
|
||||
|
||||
if (bandFiltersActive) {
|
||||
matchesBand = (
|
||||
(state.spotFilters.band160M && spot.Band === '160M') ||
|
||||
(state.spotFilters.band80M && spot.Band === '80M') ||
|
||||
(state.spotFilters.band60M && spot.Band === '60M') ||
|
||||
(state.spotFilters.band40M && spot.Band === '40M') ||
|
||||
(state.spotFilters.band30M && spot.Band === '30M') ||
|
||||
(state.spotFilters.band20M && spot.Band === '20M') ||
|
||||
(state.spotFilters.band17M && spot.Band === '17M') ||
|
||||
(state.spotFilters.band15M && spot.Band === '15M') ||
|
||||
(state.spotFilters.band12M && spot.Band === '12M') ||
|
||||
(state.spotFilters.band10M && spot.Band === '10M') ||
|
||||
(state.spotFilters.band6M && spot.Band === '6M')
|
||||
);
|
||||
}
|
||||
|
||||
if (typeFiltersActive) {
|
||||
if (state.spotFilters.showNewDXCC) {
|
||||
const isNewDXCC = !!spot.NewDXCC;
|
||||
const isNewBand = !!spot.NewBand;
|
||||
const isNewMode = !!spot.NewMode;
|
||||
if (isNewDXCC && !(isNewBand && isNewMode)) {
|
||||
matchesType = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (state.spotFilters.showNewBandMode && !!spot.NewBand && !!spot.NewMode) matchesType = true;
|
||||
if (state.spotFilters.showNewBand && !!spot.NewBand && !spot.NewMode) matchesType = true;
|
||||
if (state.spotFilters.showNewMode && !!spot.NewMode && !spot.NewBand) matchesType = true;
|
||||
if (state.spotFilters.showNewSlot && !!spot.NewSlot && !spot.NewDXCC && !spot.NewBand && !spot.NewMode) matchesType = true;
|
||||
if (state.spotFilters.showWorked && !!spot.Worked) matchesType = true;
|
||||
}
|
||||
|
||||
if (bandFiltersActive && !typeFiltersActive) {
|
||||
return matchesBand;
|
||||
} else if (typeFiltersActive && !bandFiltersActive) {
|
||||
return matchesType;
|
||||
} else if (bandFiltersActive && typeFiltersActive) {
|
||||
return matchesBand && matchesType;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSpotFilter(filterName) {
|
||||
if (filterName === 'showAll') {
|
||||
state.spotFilters = {
|
||||
showAll: true,
|
||||
showNewDXCC: false,
|
||||
showNewBand: false,
|
||||
showNewMode: false,
|
||||
showNewBandMode: false,
|
||||
showNewSlot: false,
|
||||
showWorked: false,
|
||||
band160M: false,
|
||||
band80M: false,
|
||||
band60M: false,
|
||||
band40M: false,
|
||||
band30M: false,
|
||||
band20M: false,
|
||||
band17M: false,
|
||||
band15M: false,
|
||||
band12M: false,
|
||||
band10M: false,
|
||||
band6M: false
|
||||
};
|
||||
} else {
|
||||
state.spotFilters.showAll = false;
|
||||
state.spotFilters[filterName] = !state.spotFilters[filterName];
|
||||
|
||||
const anyActive = Object.keys(state.spotFilters).some(key =>
|
||||
key !== 'showAll' && state.spotFilters[key]
|
||||
);
|
||||
if (!anyActive) {
|
||||
state.spotFilters.showAll = true;
|
||||
}
|
||||
}
|
||||
|
||||
applySpotFilters();
|
||||
render();
|
||||
}
|
||||
|
||||
async function sendCommand() {
|
||||
const command = state.command.trim();
|
||||
if (!command) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/send-command`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ command })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
state.command = '';
|
||||
alert('Command sent successfully!');
|
||||
render();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending command:', error);
|
||||
alert('Failed to send command');
|
||||
}
|
||||
}
|
||||
|
||||
async function updateFilter(filterName, value) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/filters`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ [filterName]: value })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
state.stats.filters[filterName] = value;
|
||||
fetchData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating filters:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function shutdownApp() {
|
||||
// if (!confirm('Are you sure you want to shutdown FlexDXCluster?')) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/shutdown`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
// alert('FlexDXCluster is shutting down...');
|
||||
// Redirect to a shutdown page or close
|
||||
document.body.innerHTML = '<div class="min-h-screen flex items-center justify-center bg-slate-900 text-white"><div class="text-center"><h1 class="text-4xl font-bold mb-4">FlexDXCluster Stopped</h1><p class="text-slate-400">The application has been shut down successfully.</p></div></div>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error shutting down:', error);
|
||||
alert('Failed to shutdown application');
|
||||
}
|
||||
}
|
||||
|
||||
function getPriorityColor(spot) {
|
||||
if (spot.NewDXCC) return 'bg-green-500/20 text-green-400 border-green-500/50';
|
||||
if (spot.NewBand && spot.NewMode) return 'bg-purple-500/20 text-purple-400 border-purple-500/50';
|
||||
if (spot.NewBand) return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50';
|
||||
if (spot.NewMode) return 'bg-orange-500/20 text-orange-400 border-orange-500/50';
|
||||
if (spot.Worked) return 'bg-cyan-500/20 text-cyan-400 border-cyan-500/50';
|
||||
return 'bg-gray-500/20 text-gray-400 border-gray-500/50';
|
||||
}
|
||||
|
||||
function getStatusLabel(spot) {
|
||||
if (spot.NewDXCC) return 'New DXCC';
|
||||
if (spot.NewBand && spot.NewMode) return 'New B&M';
|
||||
if (spot.NewBand) return 'New Band';
|
||||
if (spot.NewMode) return 'New Mode';
|
||||
if (spot.NewSlot) return 'New Slot';
|
||||
if (spot.Worked) return 'Worked';
|
||||
return '';
|
||||
}
|
||||
|
||||
function countSpotsByType(type) {
|
||||
switch(type) {
|
||||
case 'newDXCC': return state.spots.filter(s => s.NewDXCC).length;
|
||||
case 'newBand': return state.spots.filter(s => s.NewBand && !s.NewMode).length;
|
||||
case 'newMode': return state.spots.filter(s => s.NewMode && !s.NewBand).length;
|
||||
case 'newBandMode': return state.spots.filter(s => s.NewBand && s.NewMode).length;
|
||||
case 'newSlot': return state.spots.filter(s => s.NewSlot && !s.NewDXCC && !s.NewBand && !s.NewMode).length;
|
||||
case 'worked': return state.spots.filter(s => s.Worked).length;
|
||||
case '160M': return state.spots.filter(s => s.Band === '160M').length;
|
||||
case '80M': return state.spots.filter(s => s.Band === '80M').length;
|
||||
case '60M': return state.spots.filter(s => s.Band === '60M').length;
|
||||
case '40M': return state.spots.filter(s => s.Band === '40M').length;
|
||||
case '30M': return state.spots.filter(s => s.Band === '30M').length;
|
||||
case '20M': return state.spots.filter(s => s.Band === '20M').length;
|
||||
case '17M': return state.spots.filter(s => s.Band === '17M').length;
|
||||
case '15M': return state.spots.filter(s => s.Band === '15M').length;
|
||||
case '12M': return state.spots.filter(s => s.Band === '12M').length;
|
||||
case '10M': return state.spots.filter(s => s.Band === '10M').length;
|
||||
case '6M': return state.spots.filter(s => s.Band === '6M').length;
|
||||
default: return state.spots.length;
|
||||
}
|
||||
}
|
||||
|
||||
function render() {
|
||||
const app = document.getElementById('app');
|
||||
|
||||
if (state.loading) {
|
||||
app.innerHTML = `
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div class="w-12 h-12 border-4 border-blue-400 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
||||
<p class="text-slate-400">Loading dashboard...</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
app.innerHTML = `
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<svg class="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.348 14.651a3.75 3.75 0 010-5.303m5.304 0a3.75 3.75 0 010 5.303m-7.425 2.122a6.75 6.75 0 010-9.546m9.546 0a6.75 6.75 0 010 9.546M5.106 18.894c-3.808-3.808-3.808-9.98 0-13.789m13.788 0c3.808 3.808 3.808 9.981 0 13.79M12 12h.008v.007H12V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">FlexDXCluster</h1>
|
||||
<p class="text-xs text-slate-400">F4BPO • ${state.stats.totalContacts} Contacts</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<div class="w-2 h-2 rounded-full ${state.stats.clusterStatus === 'connected' ? 'bg-green-500 animate-pulse' : 'bg-red-500'}"></div>
|
||||
<span class="text-slate-400">Cluster</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs">
|
||||
<div class="w-2 h-2 rounded-full ${state.stats.flexStatus === 'connected' ? 'bg-green-500 animate-pulse' : 'bg-red-500'}"></div>
|
||||
<span class="text-slate-400">Flex</span>
|
||||
</div>
|
||||
<button onclick="shutdownApp()" class="px-3 py-1.5 text-xs bg-red-600 hover:bg-red-700 rounded transition-colors flex items-center gap-1">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Shutdown
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-3 mb-3">
|
||||
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<svg class="w-6 h-6 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<div class="text-xl font-bold text-blue-400">${state.stats.totalSpots}</div>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-1">Total Spots</p>
|
||||
</div>
|
||||
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<svg class="w-6 h-6 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div class="text-xl font-bold text-green-400">${state.stats.newDXCC}</div>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-1">New DXCC</p>
|
||||
</div>
|
||||
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<svg class="w-6 h-6 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<div class="text-xl font-bold text-purple-400">${state.stats.activeSpotters}</div>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-1">Spotters</p>
|
||||
</div>
|
||||
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<svg class="w-6 h-6 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
|
||||
</svg>
|
||||
<div class="text-xl font-bold text-orange-400">${state.stats.connectedClients}</div>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-1">Clients</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3 mb-3">
|
||||
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
|
||||
<div class="flex gap-2 items-center">
|
||||
<input id="commandInput" type="text" value="${state.command}" placeholder="Command..."
|
||||
class="flex-1 px-3 py-1.5 text-sm bg-slate-900/50 border border-slate-600 rounded focus:outline-none focus:border-blue-500 text-white" />
|
||||
<button onclick="sendCommand()" class="px-4 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 rounded transition-colors">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50">
|
||||
<div class="flex gap-3 text-sm">
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="checkbox" ${state.stats.filters.skimmer ? 'checked' : ''}
|
||||
onchange="updateFilter('skimmer', this.checked)" class="w-4 h-4 rounded" />
|
||||
<span>Skimmer</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="checkbox" ${state.stats.filters.ft8 ? 'checked' : ''}
|
||||
onchange="updateFilter('ft8', this.checked)" class="w-4 h-4 rounded" />
|
||||
<span>FT8</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-1.5 cursor-pointer">
|
||||
<input type="checkbox" ${state.stats.filters.ft4 ? 'checked' : ''}
|
||||
onchange="updateFilter('ft4', this.checked)" class="w-4 h-4 rounded" />
|
||||
<span>FT4</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-800/50 backdrop-blur rounded-lg p-3 border border-slate-700/50 mb-3">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<h3 class="text-xs font-bold text-slate-400 mb-2">TYPE FILTERS</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button onclick="toggleSpotFilter('showAll')"
|
||||
class="px-3 py-1 text-sm rounded transition-colors ${state.spotFilters.showAll ? 'bg-blue-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
All (${state.spots.length})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('showNewDXCC')"
|
||||
class="px-3 py-1 text-sm rounded transition-colors ${state.spotFilters.showNewDXCC ? 'bg-green-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
DXCC (${countSpotsByType('newDXCC')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('showNewBandMode')"
|
||||
class="px-3 py-1 text-sm rounded transition-colors ${state.spotFilters.showNewBandMode ? 'bg-purple-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
B&M (${countSpotsByType('newBandMode')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('showNewBand')"
|
||||
class="px-3 py-1 text-sm rounded transition-colors ${state.spotFilters.showNewBand ? 'bg-yellow-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
Band (${countSpotsByType('newBand')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('showNewMode')"
|
||||
class="px-3 py-1 text-sm rounded transition-colors ${state.spotFilters.showNewMode ? 'bg-orange-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
Mode (${countSpotsByType('newMode')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('showNewSlot')"
|
||||
class="px-3 py-1 text-sm rounded transition-colors ${state.spotFilters.showNewSlot ? 'bg-sky-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
Slot (${countSpotsByType('newSlot')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('showWorked')"
|
||||
class="px-3 py-1 text-sm rounded transition-colors ${state.spotFilters.showWorked ? 'bg-cyan-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
Worked (${countSpotsByType('worked')})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xs font-bold text-slate-400 mb-2">BAND FILTERS</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button onclick="toggleSpotFilter('band160M')"
|
||||
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band160M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
160M (${countSpotsByType('160M')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('band80M')"
|
||||
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band80M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
80M (${countSpotsByType('80M')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('band60M')"
|
||||
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band60M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
60M (${countSpotsByType('60M')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('band40M')"
|
||||
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band40M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
40M (${countSpotsByType('40M')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('band30M')"
|
||||
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band30M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
30M (${countSpotsByType('30M')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('band20M')"
|
||||
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band20M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
20M (${countSpotsByType('20M')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('band17M')"
|
||||
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band17M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
17M (${countSpotsByType('17M')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('band15M')"
|
||||
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band15M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
15M (${countSpotsByType('15M')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('band12M')"
|
||||
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band12M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
12M (${countSpotsByType('12M')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('band10M')"
|
||||
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band10M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
10M (${countSpotsByType('10M')})
|
||||
</button>
|
||||
<button onclick="toggleSpotFilter('band6M')"
|
||||
class="px-2 py-1 text-xs rounded transition-colors ${state.spotFilters.band6M ? 'bg-indigo-600 text-white' : 'bg-slate-700/50 text-slate-300 hover:bg-slate-700'}">
|
||||
6M (${countSpotsByType('6M')})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-3" style="height: calc(100vh - 360px);">
|
||||
<div class="col-span-3 bg-slate-800/50 backdrop-blur rounded-lg border border-slate-700/50 flex flex-col overflow-hidden">
|
||||
<div class="p-3 border-b border-slate-700/50">
|
||||
<h2 class="text-lg font-bold">Recent Spots (${state.filteredSpots.length})</h2>
|
||||
</div>
|
||||
<div class="scrollable">
|
||||
<table class="w-full">
|
||||
<thead class="bg-slate-900/50 sticky top-0">
|
||||
<tr class="text-left text-xs text-slate-400">
|
||||
<th class="p-2">DX</th>
|
||||
<th class="p-2">Freq</th>
|
||||
<th class="p-2">Band</th>
|
||||
<th class="p-2">Mode</th>
|
||||
<th class="p-2">Spotter</th>
|
||||
<th class="p-2">Time</th>
|
||||
<th class="p-2">Country</th>
|
||||
<th class="p-2">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${state.filteredSpots.map(spot => `
|
||||
<tr class="border-b border-slate-700/30 hover:bg-slate-700/30 transition-colors text-sm">
|
||||
<td class="p-2 font-bold text-blue-400">${spot.DX}</td>
|
||||
<td class="p-2 font-mono text-xs">${spot.FrequencyMhz}</td>
|
||||
<td class="p-2"><span class="px-1.5 py-0.5 bg-slate-700/50 rounded text-xs">${spot.Band}</span></td>
|
||||
<td class="p-2"><span class="px-1.5 py-0.5 bg-purple-500/20 text-purple-400 rounded text-xs">${spot.Mode}</span></td>
|
||||
<td class="p-2 text-slate-300 text-xs">${spot.SpotterCallsign}</td>
|
||||
<td class="p-2 text-slate-400 text-xs">${spot.UTCTime}</td>
|
||||
<td class="p-2 text-xs">${spot.CountryName || 'N/A'}</td>
|
||||
<td class="p-2">
|
||||
${getStatusLabel(spot) ? `<span class="px-1.5 py-0.5 rounded text-xs font-semibold border ${getPriorityColor(spot)}">${getStatusLabel(spot)}</span>` : ''}
|
||||
</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-800/50 backdrop-blur rounded-lg border border-slate-700/50 flex flex-col overflow-hidden">
|
||||
<div class="p-3 border-b border-slate-700/50">
|
||||
<h2 class="text-lg font-bold">Top Spotters</h2>
|
||||
</div>
|
||||
<div class="scrollable p-3">
|
||||
${state.topSpotters.map((spotter, index) => `
|
||||
<div class="flex items-center justify-between mb-2 p-2 bg-slate-900/30 rounded hover:bg-slate-700/30 transition-colors">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-6 h-6 bg-gradient-to-br from-blue-500 to-purple-500 rounded-full flex items-center justify-center text-xs font-bold">
|
||||
${index + 1}
|
||||
</div>
|
||||
<span class="text-sm font-semibold">${spotter.Spotter}</span>
|
||||
</div>
|
||||
<span class="text-slate-400 font-mono text-xs">${spotter.NumberofSpots}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const commandInput = document.getElementById('commandInput');
|
||||
if (commandInput) {
|
||||
commandInput.addEventListener('input', (e) => {
|
||||
state.command = e.target.value;
|
||||
});
|
||||
commandInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') sendCommand();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
setInterval(fetchData, 1000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
70
utils.go
Normal file
70
utils.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func FreqMhztoHz(freq string) string {
|
||||
frequency, err := strconv.ParseFloat(freq, 64)
|
||||
if err != nil {
|
||||
log.Println("could not convert frequency string to int", err)
|
||||
}
|
||||
|
||||
frequency = frequency / 1000
|
||||
|
||||
return strconv.FormatFloat(frequency, 'f', 6, 64)
|
||||
}
|
||||
|
||||
func FreqHztoMhz(freq string) string {
|
||||
frequency, err := strconv.ParseFloat(freq, 64)
|
||||
if err != nil {
|
||||
log.Println("could not convert frequency string to int", err)
|
||||
}
|
||||
|
||||
frequency = frequency * 1000
|
||||
|
||||
return strconv.FormatFloat(frequency, 'f', 6, 64)
|
||||
}
|
||||
|
||||
func CheckSignal(TCPClient *TCPClient, TCPServer *TCPServer, FlexClient *FlexClient, fRepo *FlexDXClusterRepository, cRepo *Log4OMContactsRepository) {
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
|
||||
|
||||
// Gracely closing all connextions if signal is received
|
||||
for sig := range sigCh {
|
||||
Log.Infof("received signal: %v, shutting down all connections.", sig)
|
||||
|
||||
TCPClient.Close()
|
||||
TCPServer.Conn.Close()
|
||||
FlexClient.Conn.Close()
|
||||
|
||||
if err := fRepo.db.Close(); err != nil {
|
||||
Log.Error("failed to close the database connection properly")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := cRepo.db.Close(); err != nil {
|
||||
Log.Error("failed to close Log4OM database connection properly")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
func SendUDPMessage(message string) {
|
||||
conn, err := net.Dial("udp", "127.0.0.1:2241")
|
||||
if err != nil {
|
||||
fmt.Printf("Some error %v", err)
|
||||
return
|
||||
}
|
||||
conn.Write([]byte(message))
|
||||
conn.Close()
|
||||
}
|
139
xml.go
Normal file
139
xml.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type Countries struct {
|
||||
XMLName xml.Name `xml:"Countries"`
|
||||
Countries []Country `xml:"Country"`
|
||||
}
|
||||
|
||||
type Country struct {
|
||||
XMLName xml.Name `xml:"Country"`
|
||||
ArrlPrefix string `xml:"ArrlPrefix"`
|
||||
Comment string `xml:"Comment"`
|
||||
Continent string `xml:"Continent"`
|
||||
CountryName string `xml:"CountryName"`
|
||||
CqZone string `xml:"CqZone"`
|
||||
CqZoneList string `xml:"CqZoneList"`
|
||||
Dxcc string `xml:"Dxcc"`
|
||||
ItuZone string `xml:"ItuZone"`
|
||||
IaruRegion string `xml:"IaruRegion"`
|
||||
ItuZoneList string `xml:"ItuZoneList"`
|
||||
Latitude string `xml:"Latitude"`
|
||||
Longitude string `xml:"Longitude"`
|
||||
Active string `xml:"Active"`
|
||||
CountryTag string `xml:"CountryTag"`
|
||||
CountryPrefixList CountryPrefixList `xml:"CountryPrefixList"`
|
||||
}
|
||||
|
||||
type CountryPrefixList struct {
|
||||
XMLName xml.Name `xml:"CountryPrefixList"`
|
||||
CountryPrefixList []CountryPrefix `xml:"CountryPrefix"`
|
||||
}
|
||||
|
||||
type CountryPrefix struct {
|
||||
XMLName xml.Name `xml:"CountryPrefix"`
|
||||
PrefixList string `xml:"PrefixList"`
|
||||
StartDate string `xml:"StartDate"`
|
||||
EndDate string `xml:"EndDate"`
|
||||
}
|
||||
|
||||
type DXCC struct {
|
||||
Callsign string
|
||||
CountryName string
|
||||
DXCC string
|
||||
RegEx string
|
||||
RegExSplit []string
|
||||
RegExCharacters int
|
||||
Ended bool
|
||||
}
|
||||
|
||||
func LoadCountryFile() Countries {
|
||||
// Open our xmlFile
|
||||
xmlFile, err := os.Open("country.xml")
|
||||
// if we os.Open returns an error then handle it
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// defer the closing of our xmlFile so that we can parse it later on
|
||||
defer xmlFile.Close()
|
||||
|
||||
// read our opened xmlFile as a byte array.
|
||||
byteValue, _ := io.ReadAll(xmlFile)
|
||||
|
||||
var countries Countries
|
||||
|
||||
xml.Unmarshal(byteValue, &countries)
|
||||
return countries
|
||||
}
|
||||
|
||||
func GetDXCC(dxCall string, Countries Countries) DXCC {
|
||||
DXCCList := []DXCC{}
|
||||
d := DXCC{}
|
||||
|
||||
// Get all the matching DXCC for current callsign
|
||||
for i := 0; i < len(Countries.Countries); i++ {
|
||||
regExp := regexp.MustCompile(Countries.Countries[i].CountryPrefixList.CountryPrefixList[len(Countries.Countries[i].CountryPrefixList.CountryPrefixList)-1].PrefixList)
|
||||
|
||||
match := regExp.FindStringSubmatch(dxCall)
|
||||
if len(match) != 0 {
|
||||
|
||||
d = DXCC{
|
||||
Callsign: dxCall,
|
||||
CountryName: Countries.Countries[i].CountryName,
|
||||
DXCC: Countries.Countries[i].Dxcc,
|
||||
RegEx: Countries.Countries[i].CountryPrefixList.CountryPrefixList[len(Countries.Countries[i].CountryPrefixList.CountryPrefixList)-1].PrefixList,
|
||||
}
|
||||
|
||||
if Countries.Countries[i].CountryPrefixList.CountryPrefixList[len(Countries.Countries[i].CountryPrefixList.CountryPrefixList)-1].EndDate == "" {
|
||||
d.Ended = false
|
||||
} else {
|
||||
d.Ended = true
|
||||
}
|
||||
|
||||
DXCCList = append(DXCCList, d)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < len(DXCCList); i++ {
|
||||
DXCCList[i].RegExSplit = strings.Split(DXCCList[i].RegEx, "|")
|
||||
|
||||
for j := 0; j < len(DXCCList[i].RegExSplit); j++ {
|
||||
regExp := regexp.MustCompile(DXCCList[i].RegExSplit[j])
|
||||
matched := regExp.FindStringSubmatch(dxCall)
|
||||
if len(matched) > 0 {
|
||||
DXCCList[i].RegExCharacters = utf8.RuneCountInString(DXCCList[i].RegExSplit[j])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(DXCCList) > 0 {
|
||||
DXCCMatch := DXCCList[0]
|
||||
higherMatch := 0
|
||||
|
||||
if len(DXCCList) > 1 {
|
||||
for i := 0; i < len(DXCCList); i++ {
|
||||
if DXCCList[i].RegExCharacters > higherMatch && !DXCCList[i].Ended {
|
||||
DXCCMatch = DXCCList[i]
|
||||
higherMatch = DXCCList[i].RegExCharacters
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DXCCMatch = DXCCList[0]
|
||||
}
|
||||
|
||||
return DXCCMatch
|
||||
} else {
|
||||
Log.Errorf("Could not find %s in country list", dxCall)
|
||||
}
|
||||
|
||||
return DXCC{}
|
||||
}
|
Reference in New Issue
Block a user