This commit is contained in:
Gregory Salaun 2024-09-29 18:46:39 +07:00
parent 12cd1d9d14
commit 5db76a7c6e
11 changed files with 1565 additions and 0 deletions

119
HTTPServer.go Normal file
View File

@ -0,0 +1,119 @@
package main
import (
"fmt"
"html/template"
"net/http"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus"
)
var tmpl *template.Template
var listNew = []New{}
type New struct {
DX string
Status string
NewDXCC bool
NewMode bool
NewBand bool
}
type HTTPServer struct {
router *mux.Router
Log4OMRepo Log4OMContactsRepository
Repo FlexDXClusterRepository
Log *log.Logger
FlexClient FlexClient
}
func NewHTTPServer(cRepo Log4OMContactsRepository, fRepo FlexDXClusterRepository, fClient FlexClient, log *log.Logger) *HTTPServer {
gRouter := mux.NewRouter()
return &HTTPServer{
router: gRouter,
Log4OMRepo: cRepo,
Repo: fRepo,
Log: log,
FlexClient: fClient,
}
}
func (s *HTTPServer) SetRoutes() {
s.router.HandleFunc("/", s.Homepage)
s.router.HandleFunc("/spots", s.GetSpots).Methods("GET")
s.router.HandleFunc("/spotscount", s.GetSpotsCount).Methods("GET")
s.router.HandleFunc("/spotters", s.GetSpotters).Methods("GET")
s.router.HandleFunc("/new", s.GetNew).Methods("GET")
}
func (s *HTTPServer) StartHTTPServer() {
go func() {
for spot := range s.FlexClient.FlexSpotChan {
s.GetListofNew(spot)
}
}()
tmpl, _ = template.ParseGlob("templates/*.html")
s.SetRoutes()
s.Log.Infof("starting HTTP server on %s:%s", Cfg.HTTPServer.Host, Cfg.HTTPServer.Port)
err := http.ListenAndServe(Cfg.HTTPServer.Host+":"+Cfg.HTTPServer.Port, s.router)
if err != nil {
s.Log.Warn("cannot start HTTP server: ", err)
}
}
func (s *HTTPServer) Homepage(w http.ResponseWriter, r *http.Request) {
err := tmpl.ExecuteTemplate(w, "home.html", nil)
if err != nil {
s.Log.Error("error executing home template: ", err)
}
}
func (s *HTTPServer) GetSpots(w http.ResponseWriter, r *http.Request) {
spots := s.Repo.GetAllSpots("25")
tmpl.ExecuteTemplate(w, "spot", spots)
}
func (s *HTTPServer) GetSpotsCount(w http.ResponseWriter, r *http.Request) {
spots := s.Repo.GetAllSpots("0")
count := len(spots)
tmpl.ExecuteTemplate(w, "spotCount", count)
}
func (s *HTTPServer) GetSpotters(w http.ResponseWriter, r *http.Request) {
spotters := s.Repo.GetSpotters()
tmpl.ExecuteTemplate(w, "spotters", spotters)
}
func (s *HTTPServer) GetNew(w http.ResponseWriter, r *http.Request) {
tmpl.ExecuteTemplate(w, "new", listNew)
}
func (s *HTTPServer) GetListofNew(spot FlexSpot) {
new := New{}
new.DX = spot.DX
if spot.NewDXCC {
new.Status = fmt.Sprintf("New DXCC (%s) (%s)", spot.Band, spot.Mode)
new.NewDXCC = true
} else if !spot.NewBand && spot.NewMode && spot.Mode != "" {
new.Status = fmt.Sprintf("New Mode (%s) (%s)", spot.Band, spot.Mode)
new.NewMode = true
} else if spot.NewBand && spot.NewMode && spot.Mode != "" {
new.Status = fmt.Sprintf("New Band (%s) & Mode (%s)", spot.Band, spot.Mode)
new.NewBand = true
new.NewMode = true
}
if new.Status != "" {
if len(listNew) > 10 {
listNew = append(listNew[:0], listNew[1:]...)
}
listNew = append(listNew, new)
}
}

171
TCPClient.go Normal file
View File

@ -0,0 +1,171 @@
package main
import (
"bufio"
"net"
"os"
"regexp"
"strings"
"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 count int = 0
type TCPClient struct {
Login string
Password string
Address string
Port string
Timeout time.Duration
LogWriter *bufio.Writer
Reader *bufio.Reader
Writer *bufio.Writer
Conn *net.TCPConn
TCPServer TCPServer
FlexClient FlexClient
MsgChan chan string
CmdChan chan string
SpotChan chan TelnetSpot
Log *log.Logger
Config *Config
}
func NewTCPClient(TCPServer *TCPServer, FlexClient *FlexClient, log *log.Logger) *TCPClient {
return &TCPClient{
Address: Cfg.Cluster.Server,
Port: Cfg.Cluster.Port,
Login: Cfg.Cluster.Login,
MsgChan: TCPServer.MsgChan,
CmdChan: TCPServer.CmdChan,
SpotChan: FlexClient.SpotChan,
Log: log,
TCPServer: *TCPServer,
FlexClient: *FlexClient,
}
}
func (c *TCPClient) setDefaultParams() {
if c.Timeout == 0 {
c.Timeout = 600 * time.Second
}
if c.LogWriter == nil {
c.LogWriter = bufio.NewWriter(os.Stdout)
}
}
func (c *TCPClient) StartClient() {
var err error
addr, err := net.ResolveTCPAddr("tcp", c.Address+":"+c.Port)
if err != nil {
c.Log.Error("cannot resolve Telnet Client address:", err)
}
c.setDefaultParams()
c.Conn, err = net.DialTCP("tcp", nil, addr)
if err != nil {
c.Log.Error("cannot connect to Telnet Client:", err)
}
c.Log.Infof("connected to DX cluster %s:%s", c.Address, c.Port)
err = c.Conn.SetKeepAlive(true)
if err != nil {
c.Log.Error("error while setting keep alive:", err)
}
c.Reader = bufio.NewReader(c.Conn)
c.Writer = bufio.NewWriter(c.Conn)
go func() {
for message := range c.TCPServer.CmdChan {
c.Log.Infof("Received DX Command: %s", message)
message := message + "\n"
c.WriteString(message)
}
}()
go c.ReadLine()
}
func (c *TCPClient) Close() {
c.Writer.WriteString("bye")
}
func (c *TCPClient) SetFilters() {
if Cfg.Cluster.FT8 {
c.Write([]byte("set/ft8\r\n"))
c.Log.Debug("FT8 is on as defined in the config file")
}
if Cfg.Cluster.Skimmer {
c.Write([]byte("set/skimmer\r\n"))
c.Log.Debug("Skimmer is on as defined in the config file")
}
if !Cfg.Cluster.FT8 {
c.Write([]byte("set/noft8\r\n"))
c.Log.Debug("FT8 is off as defined in the config file")
}
if !Cfg.Cluster.Skimmer {
c.Write([]byte("set/noskimmer\r\n"))
c.Log.Debug("Skimmer is off as defined in the config file")
}
}
func (c *TCPClient) ReadLine() {
for {
message, err := c.Reader.ReadString('\n')
if err != nil {
c.Log.Errorf("Error reading message: %s", err)
continue
}
if strings.Contains(message, "Login: \r\n") || strings.Contains(message, "Please enter your call: \r\n") {
c.Log.Debug("Found login prompt...sending callsign")
c.Write([]byte(c.Login + "\r\n"))
time.Sleep(time.Second * 2)
c.SetFilters()
time.Sleep(time.Second * 1)
if Cfg.Cluster.Command != "" {
c.WriteString(Cfg.Cluster.Command)
}
c.Log.Info("start receiving spots")
}
ProcessTelnetSpot(spotRe, message, c.SpotChan, c.Log)
// Send the spot message to TCP server
if len(c.TCPServer.Clients) > 0 {
if count == 0 {
// wait 3 seconds before sending messages to allow the client to connect
time.Sleep(time.Second * 3)
count++
}
c.MsgChan <- message
}
}
}
// Write sends raw data to remove telnet server
func (tc *TCPClient) Write(data []byte) (n int, err error) {
n, err = tc.Writer.Write(data)
if err == nil {
err = tc.Writer.Flush()
}
return
}
func (tc *TCPClient) WriteString(data string) (n int, err error) {
n, err = tc.Writer.Write([]byte(data))
if err == nil {
err = tc.Writer.Flush()
}
return
}

129
TCPServer.go Normal file
View File

@ -0,0 +1,129 @@
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
"sync"
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
}
func NewTCPServer(address string, port string, log *log.Logger) *TCPServer {
return &TCPServer{
Address: address,
Port: port,
Clients: make(map[net.Conn]bool),
MsgChan: make(chan string),
CmdChan: make(chan string),
Log: log,
}
}
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 {
s.Log.Info("could not create telnet server")
}
defer s.Listener.Close()
s.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()
s.Log.Info("client connected", s.Conn.RemoteAddr().String())
if err != nil {
s.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()
s.Log.Infof("client %s disconnected", s.Conn.RemoteAddr().String())
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.Log.Infof("client %s disconnected", s.Conn.RemoteAddr().String())
}
if strings.Contains(message, "DX") && message != "SH/DX 30" {
// 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()
for client := range s.Clients {
_, err := client.Write([]byte(message))
if err != nil {
fmt.Println("error while sending message to clients:", client.RemoteAddr())
}
}
}

24
clublog.go Normal file
View File

@ -0,0 +1,24 @@
package main
import (
"fmt"
"io"
"log"
"net/http"
)
func CheckClubogDXCC(callsign string) (string, error) {
// Clublog check DXCC
clublogURL := "https://clublog.org/dxcc?call=" + callsign + "&api=5767f19333363a9ef432ee9cd4141fe76b8adf38"
resp, err := http.Get(clublogURL)
if err != nil {
fmt.Println("error while getting DXCC from Clublog")
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Println("could not get dxcc from clublog", err)
}
return string(body), nil
}

82
config.go Normal file
View File

@ -0,0 +1,82 @@
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"`
LogLevel string `yaml:"log_level"`
HTTPServer bool `yaml:"httpserver"`
TelnetServer bool `yaml:"telnetserver"`
FlexRadioSpot bool `yaml:"flexradiospot"`
} `yaml:"general"`
SQLite struct {
SQLitePath string `yaml:"sqlite_path"`
Callsign string `yaml:"callsign"`
} `yaml:"sqlite"`
Cluster struct {
Server string `yaml:"server"`
Port string `yaml:"port"`
Login string `yaml:"login"`
Skimmer bool `yaml:"skimmer"`
FT8 bool `yaml:"ft8"`
Command string `yanl:"command"`
} `yaml:"cluster"`
Flex struct {
IP string `yaml:"ip"`
SpotLife string `yaml:"spot_life"`
} `yaml:"flex"`
Clublog struct {
Api string `yaml:"api"`
} `yaml:"clublog"`
TelnetServer struct {
Host string `yaml:"host"`
Port string `yaml:"port"`
} `yaml:"telnetserver"`
HTTPServer struct {
Host string `yaml:"host"`
Port string `yaml:"port"`
} `yaml:"httpserver"`
}
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
}

386
database.go Normal file
View File

@ -0,0 +1,386 @@
package main
import (
"context"
"database/sql"
"fmt"
"os"
"strconv"
"time"
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, log *log.Logger) *Log4OMContactsRepository {
db, err := sql.Open("sqlite3", filePath)
if err != nil {
fmt.Println("Cannot open db", err)
}
return &Log4OMContactsRepository{
db: db,
Log: log}
}
func NewFlexDXDatabase(filePath string, log *log.Logger) *FlexDXClusterRepository {
db, err := sql.Open("sqlite3", filePath)
if err != nil {
fmt.Println("Cannot open db", err)
}
log.Info("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" INTEGER,
"flexMode" TEXT,
"source" TEXT,
"time" TEXT,
"timestamp" INTEGER,
"lifeTime" TEXT,
"priority" TEXT,
"comment" TEXT,
"color" TEXT,
"backgroundColor" INTEGER,
PRIMARY KEY("id" AUTOINCREMENT)
)`,
)
if err != nil {
log.Warn("Cannot create table", err)
}
return &FlexDXClusterRepository{
db: db,
Log: log,
}
}
func (r *Log4OMContactsRepository) ListByCountry(countryID string) ([]*Contact, error) {
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)
return nil, 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)
return nil, err
}
contacts = append(contacts, &c)
}
return contacts, nil
}
func (r *Log4OMContactsRepository) ListByCountryMode(countryID string, mode string) ([]*Contact, error) {
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 = ?)", countryID, "USB", "LSB")
if err != nil {
log.Error("could not query database", err)
return nil, 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)
return nil, err
}
contacts = append(contacts, &c)
}
return contacts, nil
} 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)
return nil, 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)
return nil, err
}
contacts = append(contacts, &c)
}
return contacts, nil
}
}
func (r *Log4OMContactsRepository) ListByCountryBand(countryID string, band string) ([]*Contact, error) {
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)
return nil, 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)
return nil, err
}
contacts = append(contacts, &c)
}
return contacts, nil
}
func (r *Log4OMContactsRepository) ListByCallSign(callSign string, band string, mode string) ([]*Contact, error) {
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)
return nil, 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)
return nil, err
}
contacts = append(contacts, &c)
}
return contacts, nil
}
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); 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 7")
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); 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`) 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)
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 = ? 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.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); 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); 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); 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.Info("deleting existing database")
}
}

268
flexradio.go Normal file
View File

@ -0,0 +1,268 @@
package main
import (
"bufio"
"fmt"
"net"
"os"
"regexp"
"strings"
"time"
log "github.com/sirupsen/logrus"
)
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
Worked bool
}
type FlexClient struct {
Address string
Port string
Timeout time.Duration
LogWriter *bufio.Writer
Reader *bufio.Reader
Writer *bufio.Writer
Conn *net.TCPConn
SpotChan chan TelnetSpot
MsgChan chan string
FlexSpotChan chan FlexSpot
Repo FlexDXClusterRepository
Log *log.Logger
TCPServer TCPServer
}
func NewFlexClient(repo FlexDXClusterRepository, TCPServer TCPServer, log *log.Logger) *FlexClient {
return &FlexClient{
Address: Cfg.Flex.IP,
Port: "4992",
SpotChan: make(chan TelnetSpot),
FlexSpotChan: make(chan FlexSpot),
MsgChan: TCPServer.MsgChan,
Repo: repo,
TCPServer: TCPServer,
Log: log,
}
}
func (fc *FlexClient) StartFlexClient() {
var err error
addr, err := net.ResolveTCPAddr("tcp", fc.Address+":"+fc.Port)
if err != nil {
fc.Log.Error("cannot resolve Telnet Client address:", err)
}
fc.LogWriter = bufio.NewWriter(os.Stdout)
fc.Timeout = 600 * time.Second
fc.Conn, err = net.DialTCP("tcp", nil, addr)
if err != nil {
fc.Log.Error("could not connect to flex radio, exiting...", err)
os.Exit(1)
}
fc.Log.Infof("connected to flex radio at %s:%s", fc.Address, fc.Port)
go func() {
for message := range fc.SpotChan {
fc.SendSpottoFlex(message)
}
}()
fc.Reader = bufio.NewReader(fc.Conn)
fc.Writer = bufio.NewWriter(fc.Conn)
err = fc.Conn.SetKeepAlive(true)
if err != nil {
fc.Log.Error("error while setting keep alive:", err)
}
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++
fc.Log.Debug("Subscribed to spot on FlexRadio and Deleted all spots from panadapter")
}
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: "#eaeaea",
BackgroundColor: "#000000",
Priority: "5",
NewDXCC: spot.NewDXCC,
NewBand: spot.NewBand,
NewMode: spot.NewMode,
Worked: spot.CallsignWorked,
}
// If new DXCC
if spot.NewDXCC {
flexSpot.Color = "#3bf908"
flexSpot.Priority = "1"
flexSpot.BackgroundColor = "#000000"
} else if spot.DX == Cfg.SQLite.Callsign {
flexSpot.Color = "#ff0000"
flexSpot.Priority = "1"
flexSpot.BackgroundColor = "#000000"
} else if spot.CallsignWorked {
flexSpot.Color = "#000000"
flexSpot.BackgroundColor = "#00c0c0"
flexSpot.Priority = "5"
} else if spot.NewMode {
flexSpot.Color = "#f9a908"
flexSpot.Priority = "1"
flexSpot.BackgroundColor = "#000000"
} else if spot.NewBand {
flexSpot.Color = "#f9f508"
flexSpot.Priority = "1"
flexSpot.BackgroundColor = "#000000"
} else if !spot.NewBand && !spot.NewMode && !spot.NewDXCC && !spot.CallsignWorked {
flexSpot.Color = "#eaeaea"
flexSpot.Priority = "5"
flexSpot.BackgroundColor = "#000000"
}
flexSpot.Comment = flexSpot.Comment + "[" + flexSpot.Mode + "] [" + flexSpot.SpotterCallsign + "]"
flexSpot.Comment = strings.ReplaceAll(flexSpot.Comment, " ", "\u00A0")
srcFlexSpot, err := fc.Repo.FindDXSameBand(flexSpot)
if err != nil {
fc.Log.Error("could not find the DX in the database: ", err)
}
// send FlexSpot to HTTP Server
fc.FlexSpotChan <- flexSpot
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 && srcFlexSpot.FrequencyMhz != flexSpot.FrequencyMhz {
fc.Repo.UpdateSpotSameBand(flexSpot)
stringSpot = fmt.Sprintf("C%v|spot set %v 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, srcFlexSpot.FlexSpotNumber, 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++
}
if Cfg.General.FlexRadioSpot {
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 {
fc.Log.Errorf("error reading message from flexradio closing program: %s", err)
os.Exit(1)
}
// fc.Log.Info(message)
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 {
fc.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 {
fc.Log.Errorf("could not find spot by flex spot number in database: %s", err)
}
msg := fmt.Sprintf(`To ALL de %s <%s> : Clicked on "%s" at %s`, Cfg.SQLite.Callsign, spot.UTCTime, spot.DX, spot.FrequencyHz)
if len(fc.TCPServer.Clients) > 0 {
fc.MsgChan <- msg
fc.Log.Infof("%s clicked on spot \"%s\" at %s", Cfg.SQLite.Callsign, spot.DX, spot.FrequencyMhz)
}
}
// Status when a spot is deleted
regSpotDeleted := *regexp.MustCompile(`S\d+\|spot (\d+) removed`)
respDelete := regSpotDeleted.FindStringSubmatch(message)
if len(respDelete) > 0 {
spot, _ := fc.Repo.FindSpotByFlexSpotNumber(respDelete[1])
fc.Repo.DeleteSpotByFlexSpotNumber(respDelete[1])
fc.Log.Infof("Spot: DX: %s - Spotter: %s - Freq: %s - Band: %s - FlexID: %v deleted from database", spot.DX, spot.SpotterCallsign, spot.FrequencyHz, spot.Band, 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
}

70
log.go Normal file
View File

@ -0,0 +1,70 @@
package main
import (
"io"
"os"
log "github.com/sirupsen/logrus"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
)
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)
}
l := &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" {
l.Level = log.DebugLevel
} else if Cfg.General.LogLevel == "INFO" {
l.Level = log.InfoLevel
} else if Cfg.General.LogLevel == "WARN" {
l.Level = log.WarnLevel
} else {
l.Level = log.InfoLevel
}
return l
}
// 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...)
}

87
main.go Normal file
View File

@ -0,0 +1,87 @@
package main
import (
"flag"
"log"
"os"
"os/signal"
"syscall"
)
func ParseFlags() (string, error) {
// String that contains the configured configuration path
var configPath string
// Set up a CLI flag called "-config" to allow users
// to supply the configuration file
flag.StringVar(&configPath, "config", "./config.yml", "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("config loaded.")
log.Infof("Callsign: %s", cfg.SQLite.Callsign)
DeleteDatabase("./flex.sqlite", log)
fRepo := NewFlexDXDatabase("flex.sqlite", log)
defer fRepo.db.Close()
cRepo := NewLog4OMContactsRepository(cfg.SQLite.SQLitePath, log)
defer cRepo.db.Close()
TCPServer := NewTCPServer(cfg.TelnetServer.Host, cfg.TelnetServer.Port, log)
FlexClient := NewFlexClient(*fRepo, *TCPServer, log)
TCPClient := NewTCPClient(TCPServer, FlexClient, log)
HTTPServer := NewHTTPServer(*cRepo, *fRepo, *FlexClient, log)
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
go FlexClient.StartFlexClient()
go TCPClient.StartClient()
go TCPServer.StartServer()
go HTTPServer.StartHTTPServer()
for sig := range sigCh {
log.Infof("received signal: %v, shutting down TCP Client.", sig)
TCPClient.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)
}
}

195
spot.go Normal file
View File

@ -0,0 +1,195 @@
package main
import (
"fmt"
"regexp"
"strconv"
"strings"
_ "github.com/mattn/go-sqlite3"
log "github.com/sirupsen/logrus"
)
type TelnetSpot struct {
DX string
Spotter string
Frequency string
Mode string
Band string
Time string
DXCC string
Comment string
CommandNumber int
FlexSpotNumber int
NewDXCC bool
NewBand bool
NewMode bool
CallsignWorked bool
}
func ProcessTelnetSpot(re *regexp.Regexp, spotRaw string, SpotChan chan TelnetSpot, log *log.Logger) {
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],
}
spot.GetBand()
spot.GuessMode()
spot.DXCC, _ = CheckClubogDXCC(spot.DX)
spot.CallsignWorked = false
spot.NewBand = false
spot.NewMode = false
spot.NewDXCC = false
contactRepo := NewLog4OMContactsRepository(Cfg.SQLite.SQLitePath, log)
defer contactRepo.db.Close()
contacts, _ := contactRepo.ListByCountry(spot.DXCC)
contactsMode, _ := contactRepo.ListByCountryMode(spot.DXCC, spot.Mode)
contactsBand, _ := contactRepo.ListByCountryBand(spot.DXCC, spot.Band)
contactsCall, _ := contactRepo.ListByCallSign(spot.DX, spot.Band, spot.Mode)
if len(contacts) == 0 {
switch spot.DXCC {
case "997":
spot.NewDXCC = false
case "1000":
spot.NewDXCC = false
default:
spot.NewDXCC = true
}
} else if len(contactsMode) == 0 {
spot.NewMode = true
} else if len(contactsBand) == 0 {
spot.NewBand = true
} else if len(contactsCall) > 0 {
spot.CallsignWorked = true
}
if spot.NewDXCC {
log.Debugf("(** New DXCC **) DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - Command: %v, FlexSpot: %v",
spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.CommandNumber, spot.FlexSpotNumber)
}
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.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 {
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)
}
// send spot to SpotChan to Flex Client to send the spot to Flex radio
SpotChan <- spot
}
}
func (spot *TelnetSpot) GetBand() {
switch true {
case strings.HasPrefix(spot.Frequency, "1.8"):
spot.Band = "160M"
if spot.Mode == "SSB" {
spot.Mode = "LSB"
}
case strings.HasPrefix(spot.Frequency, "3"):
spot.Band = "80M"
if spot.Mode == "SSB" {
spot.Mode = "LSB"
}
case strings.HasPrefix(spot.Frequency, "7"):
spot.Band = "40M"
if spot.Mode == "SSB" {
spot.Mode = "LSB"
}
case strings.HasPrefix(spot.Frequency, "10"):
spot.Band = "30M"
case strings.HasPrefix(spot.Frequency, "14"):
spot.Band = "20M"
if spot.Mode == "SSB" {
spot.Mode = "USB"
}
case strings.HasPrefix(spot.Frequency, "18"):
spot.Band = "17M"
if spot.Mode == "SSB" {
spot.Mode = "USB"
}
case strings.HasPrefix(spot.Frequency, "21"):
spot.Band = "15M"
if spot.Mode == "SSB" {
spot.Mode = "USB"
}
case strings.HasPrefix(spot.Frequency, "24"):
spot.Band = "12M"
if spot.Mode == "SSB" {
spot.Mode = "USB"
}
case strings.HasPrefix(spot.Frequency, "28"):
spot.Band = "10M"
if spot.Mode == "SSB" {
spot.Mode = "USB"
}
case strings.HasPrefix(spot.Frequency, "29"):
spot.Band = "10M"
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 {
fmt.Println("could not convert frequency string in float64:", err)
}
switch spot.Band {
case "160M":
if freqInt <= 1840 && freqInt >= 1800 {
spot.Mode = "CW"
}
case "40M":
if freqInt <= 7045.49 && freqInt >= 7000 {
spot.Mode = "CW"
} else if freqInt <= 7048.49 && freqInt >= 7045.49 {
spot.Mode = "FT4"
} else if freqInt <= 7073.99 && freqInt > 7048.49 {
spot.Mode = "CW"
} else if freqInt <= 7077 && freqInt > 7073.99 {
spot.Mode = "FT8"
} else if freqInt <= 7200 && freqInt > 7077 {
spot.Mode = "LSB"
}
}
}
}

34
utils.go Normal file
View File

@ -0,0 +1,34 @@
package main
import (
"log"
"math"
"strconv"
)
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 roundFloat(val float64, precision uint) float64 {
ratio := math.Pow(10, float64(precision))
return math.Round(val*ratio) / ratio
}