From 5db76a7c6e3b7cdbe4f1806284c511ed202de250 Mon Sep 17 00:00:00 2001 From: Greg Salaun Date: Sun, 29 Sep 2024 18:46:39 +0700 Subject: [PATCH] update --- HTTPServer.go | 119 ++++++++++++++++ TCPClient.go | 171 ++++++++++++++++++++++ TCPServer.go | 129 +++++++++++++++++ clublog.go | 24 ++++ config.go | 82 +++++++++++ database.go | 386 ++++++++++++++++++++++++++++++++++++++++++++++++++ flexradio.go | 268 +++++++++++++++++++++++++++++++++++ log.go | 70 +++++++++ main.go | 87 ++++++++++++ spot.go | 195 +++++++++++++++++++++++++ utils.go | 34 +++++ 11 files changed, 1565 insertions(+) create mode 100644 HTTPServer.go create mode 100644 TCPClient.go create mode 100644 TCPServer.go create mode 100644 clublog.go create mode 100644 config.go create mode 100644 database.go create mode 100644 flexradio.go create mode 100644 log.go create mode 100644 main.go create mode 100644 spot.go create mode 100644 utils.go diff --git a/HTTPServer.go b/HTTPServer.go new file mode 100644 index 0000000..fffbd9b --- /dev/null +++ b/HTTPServer.go @@ -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) + } +} diff --git a/TCPClient.go b/TCPClient.go new file mode 100644 index 0000000..cc615b8 --- /dev/null +++ b/TCPClient.go @@ -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 +} diff --git a/TCPServer.go b/TCPServer.go new file mode 100644 index 0000000..ddc5b4b --- /dev/null +++ b/TCPServer.go @@ -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()) + } + } +} diff --git a/clublog.go b/clublog.go new file mode 100644 index 0000000..8de66c1 --- /dev/null +++ b/clublog.go @@ -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 +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..a7e3881 --- /dev/null +++ b/config.go @@ -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 +} diff --git a/database.go b/database.go new file mode 100644 index 0000000..f8a3d3a --- /dev/null +++ b/database.go @@ -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") + } +} diff --git a/flexradio.go b/flexradio.go new file mode 100644 index 0000000..cae4b20 --- /dev/null +++ b/flexradio.go @@ -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 +} diff --git a/log.go b/log.go new file mode 100644 index 0000000..2b96e3b --- /dev/null +++ b/log.go @@ -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...) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..f051ad3 --- /dev/null +++ b/main.go @@ -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) + } + +} diff --git a/spot.go b/spot.go new file mode 100644 index 0000000..19f8a02 --- /dev/null +++ b/spot.go @@ -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" + } + + } + } +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..024fba9 --- /dev/null +++ b/utils.go @@ -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 +}