This commit is contained in:
Gregory Salaun 2024-09-23 16:24:50 +07:00
parent 20952252be
commit 9ddc3ce347
14 changed files with 195878 additions and 0 deletions

123
TCPClient.go Normal file
View File

@ -0,0 +1,123 @@
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})`)
type TelnetClient 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
}
func (tc *TelnetClient) setDefaultParams() {
if tc.Timeout == 0 {
tc.Timeout = 600 * time.Second
}
if tc.LogWriter == nil {
tc.LogWriter = bufio.NewWriter(os.Stdout)
}
}
func (tc *TelnetClient) StartClient() {
var err error
addr, err := net.ResolveTCPAddr("tcp", tc.Address+":"+tc.Port)
if err != nil {
tc.Log.Error("cannot resolve Telnet Client address:", err)
}
tc.setDefaultParams()
tc.Conn, err = net.DialTCP("tcp", nil, addr)
if err != nil {
tc.Log.Error("cannot connect to Telnet Client:", err)
}
tc.Log.Infof("connected to %s:%s", tc.Address, tc.Port)
err = tc.Conn.SetKeepAlive(true)
if err != nil {
tc.Log.Error("error while setting keep alive:", err)
}
tc.Reader = bufio.NewReader(tc.Conn)
tc.Writer = bufio.NewWriter(tc.Conn)
go tc.ReadLine()
}
func (tc *TelnetClient) Close() {
tc.Writer.WriteString("bye")
}
func (tc *TelnetClient) SetFilters() {
if Cfg.Cluster.FT8 {
tc.Write([]byte("set/ft8\r\n"))
tc.Log.Info("FT8 is on as defined in the config file")
}
if Cfg.Cluster.Skimmer {
tc.Write([]byte("set/skimmer\r\n"))
tc.Log.Info("Skimmer is on as defined in the config file")
}
if !Cfg.Cluster.FT8 {
tc.Write([]byte("set/noft8\r\n"))
tc.Log.Info("FT8 is off as defined in the config file")
}
if !Cfg.Cluster.Skimmer {
tc.Write([]byte("set/noskimmer\r\n"))
tc.Log.Info("Skimmer is off as defined in the config file")
}
}
func (tc *TelnetClient) ReadLine() {
for {
message, err := tc.Reader.ReadString('\n')
if err != nil {
tc.Log.Errorf("Error reading message: %s", err)
continue
}
if strings.Contains(message, "Login: \r\n") || strings.Contains(message, "Please enter your call: \r\n") {
tc.Log.Info("Found login prompt...sending callsign")
tc.Write([]byte(tc.Login + "\r\n"))
time.Sleep(time.Second * 2)
tc.SetFilters()
}
ProcessTelnetSpot(spotRe, message, tc.SpotChan, tc.Log)
tc.MsgChan <- message
}
}
// Write sends raw data to remove telnet server
func (tc *TelnetClient) Write(data []byte) (n int, err error) {
n, err = tc.Writer.Write(data)
if err == nil {
err = tc.Writer.Flush()
}
return
}

125
TCPServer.go Normal file
View File

@ -0,0 +1,125 @@
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
}
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.Telnet.Host+":"+Cfg.Telnet.Port)
if err != nil {
s.Log.Info("could not create telnet server")
}
defer s.Listener.Close()
s.Log.Info("telnet server listening on %s:%s", Cfg.Telnet.Host, Cfg.Telnet.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() {
defer s.Conn.Close()
s.Conn.Write([]byte("Welcome to the FlexDXCluster telnet server! Type 'bye' to exit.\n"))
// s.Conn.Write([]byte(`To ALL de XV9Q <0234Z> : Clicked on "JH2UNG"\n`))
reader := bufio.NewReader(s.Conn)
for {
message, err := reader.ReadString('\n')
if err != nil {
s.Mutex.Lock()
delete(s.Clients, s.Conn)
s.Mutex.Unlock()
s.Log.Info("client disconnected")
return
}
message = strings.TrimSpace(message)
if message == "bye" {
s.Mutex.Lock()
delete(s.Clients, s.Conn)
s.Mutex.Unlock()
s.Log.Info("client disconnected")
return
}
s.Log.Info("Message reçu du client: %s\n", message)
}
}
func (s *TCPServer) Write(message string) (n int, err error) {
n, 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("Erreur lors de l'envoi du message au client:", 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
}

69
config.go Normal file
View File

@ -0,0 +1,69 @@
package main
import (
"fmt"
"os"
"gopkg.in/yaml.v2"
)
var (
Cfg *Config
)
type Config struct {
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"`
} `yaml:"cluster"`
Flex struct {
IP string `yaml:"ip"`
SpotLife string `yaml:"spot_life"`
} `yaml:"flex"`
Clublog struct {
Api string `yaml:"api"`
} `yaml:"clublog"`
Telnet struct {
Host string `yaml:"host"`
Port string `yaml:"port"`
} `yaml:"telnet"`
}
func NewConfig(configPath string) error {
config := &Config{}
file, err := os.Open(configPath)
if err != nil {
return err
}
defer file.Close()
d := yaml.NewDecoder(file)
if err := d.Decode(&config); err != nil {
return err
}
Cfg = config
return nil
}
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
}

19
config.yml Normal file
View File

@ -0,0 +1,19 @@
general:
log_level: DEBUG
sqlite:
sqlite_path: 'C:\Perso\Seafile\Radio\Logs\Log4OM\Vietnam.SQLite'
callsign: XV9Q
cluster:
server: dxc.k0xm.net
port: 7300
login: xv9q-5
skimmer: true
ft8: false
flex:
ip: 10.10.10.120
spot_life: 300
clublog:
api: 5767f19333363a9ef432ee9cd4141fe76b8adf38
telnet:
host: 0.0.0.0
port: 7301

289
database.go Normal file
View File

@ -0,0 +1,289 @@
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 ContactsRepository struct {
db *sql.DB
Log *log.Logger
}
type FlexDXClusterRepository struct {
db *sql.DB
Log *log.Logger
}
func NewContactsRepository(filePath string, log *log.Logger) *ContactsRepository {
db, err := sql.Open("sqlite3", filePath)
if err != nil {
fmt.Println("Cannot open db", err)
}
return &ContactsRepository{
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 UNIQUE,
"dx" TEXT NOT NULL,
"freqMhz" TEXT,
"freqHz" TEXT,
"band" TEXT,
"mode" TEXT,
"spotter" INTEGER,
"flexMode" TEXT,
"source" 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 *ContactsRepository) 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
}
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 *ContactsRepository) ListByCountryMode(countryID string, mode string) ([]*Contact, error) {
modeUSB := "USB"
modeLSB := "LSB"
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, modeLSB, modeUSB)
if err != nil {
log.Error("could not query database", err)
return nil, err
}
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
}
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 *ContactsRepository) 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
}
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 *ContactsRepository) 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
}
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) FindDXSameBand(spot FlexSpot) (*FlexSpot, error) {
rows, err := r.db.Query("SELECT * from spots WHERE dx = ? AND band = ?", spot.DX, spot.Band)
if err != nil {
fmt.Println(err)
return nil, err
}
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.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`, `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, 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) {
return
}
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
}
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.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
}
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.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(flexSpot string, spot FlexSpot) (*FlexSpot, error) {
flexSpotNumberInt, _ := strconv.Atoi(flexSpot)
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)
}
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.TimeStamp, &s.LifeTime, &s.Priority,
&s.Comment, &s.Color, &s.BackgroundColor); err != nil {
fmt.Println(err)
return nil, err
}
}
return &s, nil
}
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")
}
}

BIN
flex.sqlite Normal file

Binary file not shown.

226
flexradio.go Normal file
View File

@ -0,0 +1,226 @@
package main
import (
"bufio"
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
log "github.com/sirupsen/logrus"
)
var command 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
LifeTime string
Priority string
Comment string
Color string
BackgroundColor string
}
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
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),
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 dial flex client:", err)
os.Exit(1)
}
fc.Log.Infof("connected to %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()
}
func (fc *FlexClient) SendSpottoFlex(spot TelnetSpot) (n int, err error) {
freq := FreqMhztoHz(spot.Frequency)
flexSpot := FlexSpot{
CommandNumber: command,
DX: spot.DX,
FrequencyMhz: freq,
FrequencyHz: spot.Frequency,
Band: spot.Band,
Mode: spot.Mode,
Source: "FlexDXCluster",
SpotterCallsign: spot.Spotter,
TimeStamp: time.Now().Unix(),
LifeTime: Cfg.Flex.SpotLife,
Comment: spot.Comment,
Color: "#eaeaea",
BackgroundColor: "#000000",
Priority: "5",
}
if spot.NewDXCC {
flexSpot.Color = "#3bf908"
flexSpot.Priority = "1"
flexSpot.BackgroundColor = "#000000"
}
if spot.NewBand || spot.NewMode && spot.Mode != "" {
flexSpot.Color = "#f9f508"
flexSpot.Priority = "2"
flexSpot.BackgroundColor = "#000000"
}
if !spot.NewBand && !spot.NewMode && !spot.NewDXCC && !spot.CallsignWorked {
flexSpot.Color = "#eaeaea"
flexSpot.Priority = "5"
flexSpot.BackgroundColor = "#000000"
}
if spot.CallsignWorked {
flexSpot.Color = "#000000"
flexSpot.BackgroundColor = "#00c0c0"
flexSpot.Priority = "5"
}
flexSpot.Comment = strings.ReplaceAll(flexSpot.Comment, " ", "\u00A0")
flexSpot.Comment = flexSpot.Comment + "\u00A0" + "[" + flexSpot.Mode + "]"
srcFlexSpot, err := fc.Repo.FindDXSameBand(flexSpot)
if err != nil {
fc.Log.Error("could not find the DX in the database: ", err)
}
var stringSpot string
spotLife, _ := strconv.Atoi(Cfg.Flex.SpotLife)
spotTime := time.Unix(srcFlexSpot.TimeStamp, 0)
elapsed := time.Since(spotTime)
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)
} else if srcFlexSpot.DX != "" && elapsed > time.Duration(spotLife) {
fc.Repo.UpdateSpotSameBand(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)
} else if srcFlexSpot.DX != "" && srcFlexSpot.Band == flexSpot.Band && srcFlexSpot.FrequencyMhz != flexSpot.FrequencyMhz && elapsed < time.Duration(spotLife) {
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.CommandNumber, flexSpot.FrequencyMhz,
flexSpot.DX, flexSpot.Mode, flexSpot.Source, flexSpot.SpotterCallsign, flexSpot.TimeStamp, flexSpot.LifeTime, flexSpot.Comment, flexSpot.Color, flexSpot.BackgroundColor, flexSpot.Priority)
}
fc.Write(stringSpot)
command++
return
}
func (fc *FlexClient) ReadLine() {
for {
message, err := fc.Reader.ReadString(byte('\n'))
if err != nil {
fc.Log.Errorf("error reading message: %s", err)
continue
}
// 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)
// }
// }
// regTriggerSpot := *regexp.MustCompile(`.*\|spot\s(\d+)\striggered.*`)
// 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 <0233z> : Clicked on %s at %s`, Cfg.SQLite.Callsign, spot.DX, spot.FrequencyHz)
// if len(fc.TCPServer.Clients) > 0 {
// fc.MsgChan <- msg
// }
// }
msg := strings.TrimSpace(message)
fc.Log.Info(msg)
}
}
// 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
}

194659
flexradio.log Normal file

File diff suppressed because it is too large Load Diff

19
go.mod Normal file
View File

@ -0,0 +1,19 @@
module git.rouggy.com/rouggy/FlexDXCluster
go 1.23.1
require (
github.com/mattn/go-sqlite3 v1.14.23
github.com/sirupsen/logrus v1.9.3
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/term v0.24.0 // indirect
)

35
go.sum Normal file
View File

@ -0,0 +1,35 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

63
logger/log.go Normal file
View File

@ -0,0 +1,63 @@
package logger
import (
"io"
"os"
log "github.com/sirupsen/logrus"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
)
var Log *log.Logger
func NewLog() *log.Logger {
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)
Log := &log.Logger{
Out: w,
Level: log.DebugLevel,
Formatter: &prefixed.TextFormatter{
DisableColors: false,
TimestampFormat: "02-01-2006 15:04:05",
FullTimestamp: true,
ForceFormatting: true,
},
}
return Log
}
// Info ...
func Info(format string, v ...interface{}) {
log.Infof(format, v...)
}
// Warn ...
func Warn(format string, v ...interface{}) {
log.Warnf(format, v...)
}
// Error ...
func Error(format string, v ...interface{}) {
log.Errorf(format, v...)
}
var (
// ConfigError ...
ConfigError = "%v type=config.error"
// HTTPError ...
HTTPError = "%v type=http.error"
// HTTPWarn ...
HTTPWarn = "%v type=http.warn"
// HTTPInfo ...
HTTPInfo = "%v type=http.info"
)

193
spot.go Normal file
View File

@ -0,0 +1,193 @@
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
SpotChan chan TelnetSpot
}
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 := NewContactsRepository(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.Infof("(** 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.Infof("(** 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.Infof("(** 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 {
log.Infof("(** 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.Infof("(** 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.Infof("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)
}
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 <= 7074 && freqInt >= 7000 {
spot.Mode = "CW"
} else if freqInt <= 7079 && freqInt > 7074 {
spot.Mode = "FT8"
} else if freqInt <= 7079 && freqInt > 7074 {
spot.Mode = "FT8"
} else if freqInt <= 7079 && freqInt > 7074 {
spot.Mode = "FT8"
}
}
}
}

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
}