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 +}