Compare commits
14 Commits
026915fdec
...
main
Author | SHA1 | Date | |
---|---|---|---|
4d5c810786 | |||
13fa688329 | |||
a17541c2e6 | |||
73e5da15bf | |||
b4bbd427aa | |||
f1d156ea84 | |||
ebdf1336a1 | |||
db41a32a5b | |||
b77b013d63 | |||
f3851e44b6 | |||
95d6e0c4ff | |||
d307c92d25 | |||
9226eb5b2e | |||
ded5c332e2 |
0
.vscode/launch.json
vendored
Normal file
0
.vscode/launch.json
vendored
Normal file
91
TCPClient.go
91
TCPClient.go
@ -18,10 +18,12 @@ type TCPClient struct {
|
|||||||
Password string
|
Password string
|
||||||
Address string
|
Address string
|
||||||
Port string
|
Port string
|
||||||
|
LoggedIn bool
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
LogWriter *bufio.Writer
|
LogWriter *bufio.Writer
|
||||||
Reader *bufio.Reader
|
Reader *bufio.Reader
|
||||||
Writer *bufio.Writer
|
Writer *bufio.Writer
|
||||||
|
Scanner *bufio.Scanner
|
||||||
Conn *net.TCPConn
|
Conn *net.TCPConn
|
||||||
TCPServer TCPServer
|
TCPServer TCPServer
|
||||||
MsgChan chan string
|
MsgChan chan string
|
||||||
@ -38,6 +40,7 @@ func NewTCPClient(TCPServer *TCPServer, Countries Countries) *TCPClient {
|
|||||||
Address: Cfg.Cluster.Server,
|
Address: Cfg.Cluster.Server,
|
||||||
Port: Cfg.Cluster.Port,
|
Port: Cfg.Cluster.Port,
|
||||||
Login: Cfg.Cluster.Login,
|
Login: Cfg.Cluster.Login,
|
||||||
|
Password: Cfg.Cluster.Password,
|
||||||
MsgChan: TCPServer.MsgChan,
|
MsgChan: TCPServer.MsgChan,
|
||||||
CmdChan: TCPServer.CmdChan,
|
CmdChan: TCPServer.CmdChan,
|
||||||
SpotChanToFlex: make(chan TelnetSpot, 100),
|
SpotChanToFlex: make(chan TelnetSpot, 100),
|
||||||
@ -54,6 +57,7 @@ func (c *TCPClient) setDefaultParams() {
|
|||||||
if c.LogWriter == nil {
|
if c.LogWriter == nil {
|
||||||
c.LogWriter = bufio.NewWriter(os.Stdout)
|
c.LogWriter = bufio.NewWriter(os.Stdout)
|
||||||
}
|
}
|
||||||
|
c.LoggedIn = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TCPClient) StartClient() {
|
func (c *TCPClient) StartClient() {
|
||||||
@ -69,19 +73,18 @@ func (c *TCPClient) StartClient() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
Log.Error("Cannot connect to Telnet Client:", err)
|
Log.Error("Cannot connect to Telnet Client:", err)
|
||||||
}
|
}
|
||||||
Log.Infof("Connected to DX cluster %s:%s", c.Address, c.Port)
|
|
||||||
|
|
||||||
err = c.Conn.SetKeepAlive(true)
|
// err = c.Conn.SetKeepAlive(true)
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
Log.Error("Error while setting keep alive:", err)
|
// Log.Error("Error while setting keep alive:", err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
c.Reader = bufio.NewReader(c.Conn)
|
c.Reader = bufio.NewReader(c.Conn)
|
||||||
c.Writer = bufio.NewWriter(c.Conn)
|
c.Writer = bufio.NewWriter(c.Conn)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for message := range c.TCPServer.CmdChan {
|
for message := range c.TCPServer.CmdChan {
|
||||||
Log.Infof("Received DX Command: %s", message)
|
Log.Infof("Received Command: %s", message)
|
||||||
message := message + "\n"
|
message := message + "\n"
|
||||||
c.WriteString(message)
|
c.WriteString(message)
|
||||||
}
|
}
|
||||||
@ -130,33 +133,63 @@ func (c *TCPClient) SetFilters() {
|
|||||||
func (c *TCPClient) ReadLine() {
|
func (c *TCPClient) ReadLine() {
|
||||||
|
|
||||||
for {
|
for {
|
||||||
message, err := c.Reader.ReadString('\n')
|
|
||||||
message, _ = strings.CutSuffix(message, "\n")
|
|
||||||
message, _ = strings.CutSuffix(message, "\r")
|
|
||||||
if err != nil {
|
|
||||||
Log.Errorf("Error reading message: %s", err)
|
|
||||||
c.Conn.Close()
|
|
||||||
c.StartClient()
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(message, Cfg.Cluster.LoginPrompt) {
|
// Need to check data with space first to find login and then use \n
|
||||||
Log.Debug("Found login prompt...sending callsign")
|
if !c.LoggedIn {
|
||||||
c.Write([]byte(c.Login + "\r\n"))
|
message, err := c.Reader.ReadString(' ')
|
||||||
c.SetFilters()
|
message, _ = strings.CutSuffix(message, "\n")
|
||||||
if Cfg.Cluster.Command != "" {
|
message, _ = strings.CutSuffix(message, "\r")
|
||||||
c.WriteString(Cfg.Cluster.Command)
|
|
||||||
|
if err != nil {
|
||||||
|
Log.Errorf("Error reading message: %s", err)
|
||||||
|
c.Conn.Close()
|
||||||
|
c.StartClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(message, Cfg.Cluster.LoginPrompt) {
|
||||||
|
Log.Debug("Found login prompt...sending callsign")
|
||||||
|
c.Write([]byte(c.Login + "\r\n"))
|
||||||
|
c.LoggedIn = true
|
||||||
|
// c.SetFilters()
|
||||||
|
// if Cfg.Cluster.Command != "" {
|
||||||
|
// c.WriteString(Cfg.Cluster.Command + "\n\r")
|
||||||
|
// }
|
||||||
|
Log.Infof("Connected to DX cluster %s:%s", Cfg.Cluster.Server, Cfg.Cluster.Port)
|
||||||
|
Log.Info("Start receiving spots")
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
Log.Info("Start receiving spots")
|
|
||||||
} else if strings.Contains(message, "Error reading from server: read tcp") {
|
|
||||||
Log.Error("Disconnected from Telnet Server, reconnecting")
|
|
||||||
c.Close()
|
|
||||||
c.StartClient()
|
|
||||||
} else {
|
|
||||||
ProcessTelnetSpot(spotRe, message, c.SpotChanToFlex, c.SpotChanToHTTPServer, c.Countries)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the spot message to TCP server
|
if c.LoggedIn {
|
||||||
c.MsgChan <- message
|
message, err := c.Reader.ReadString('\n')
|
||||||
|
message, _ = strings.CutSuffix(message, "\n")
|
||||||
|
message, _ = strings.CutSuffix(message, "\r")
|
||||||
|
|
||||||
|
if strings.Contains(message, "password") {
|
||||||
|
Log.Debug("Found password prompt...sending password")
|
||||||
|
c.Write([]byte(c.Password + "\r\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
Log.Errorf("Error reading message: %s", err)
|
||||||
|
c.Conn.Close()
|
||||||
|
c.StartClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(message, "Error reading from server: read tcp") {
|
||||||
|
Log.Error("Disconnected from Telnet Server, reconnecting")
|
||||||
|
c.Close()
|
||||||
|
c.StartClient()
|
||||||
|
} else {
|
||||||
|
if c.LoggedIn && strings.Contains(message, "DX") {
|
||||||
|
ProcessTelnetSpot(spotRe, message, c.SpotChanToFlex, c.SpotChanToHTTPServer, c.Countries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the spot message to TCP server
|
||||||
|
c.MsgChan <- message
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
config.go
24
config.go
@ -14,19 +14,31 @@ type Config struct {
|
|||||||
General struct {
|
General struct {
|
||||||
DeleteLogFileAtStart bool `yaml:"delete_log_file_at_start"`
|
DeleteLogFileAtStart bool `yaml:"delete_log_file_at_start"`
|
||||||
LogToFile bool `yaml:"log_to_file"`
|
LogToFile bool `yaml:"log_to_file"`
|
||||||
|
Callsign string `yaml:"callsign"`
|
||||||
LogLevel string `yaml:"log_level"`
|
LogLevel string `yaml:"log_level"`
|
||||||
TelnetServer bool `yaml:"telnetserver"`
|
TelnetServer bool `yaml:"telnetserver"`
|
||||||
FlexRadioSpot bool `yaml:"flexradiospot"`
|
FlexRadioSpot bool `yaml:"flexradiospot"`
|
||||||
} `yaml:"general"`
|
} `yaml:"general"`
|
||||||
|
|
||||||
|
Database struct {
|
||||||
|
MySQL bool `yaml:"mysql"`
|
||||||
|
SQLite bool `yaml:"sqlite"`
|
||||||
|
MySQLUser string `yaml:"mysql_db_user"`
|
||||||
|
MySQLPassword string `yaml:"mysql_db_password"`
|
||||||
|
MySQLDbName string `yaml:"mysql_db_name"`
|
||||||
|
MySQLHost string `yaml:"mysql_host"`
|
||||||
|
MySQLPort string `yaml:"mysql_port"`
|
||||||
|
} `yaml:"database"`
|
||||||
|
|
||||||
SQLite struct {
|
SQLite struct {
|
||||||
SQLitePath string `yaml:"sqlite_path"`
|
SQLitePath string `yaml:"sqlite_path"`
|
||||||
Callsign string `yaml:"callsign"`
|
|
||||||
} `yaml:"sqlite"`
|
} `yaml:"sqlite"`
|
||||||
|
|
||||||
Cluster struct {
|
Cluster struct {
|
||||||
Server string `yaml:"server"`
|
Server string `yaml:"server"`
|
||||||
Port string `yaml:"port"`
|
Port string `yaml:"port"`
|
||||||
Login string `yaml:"login"`
|
Login string `yaml:"login"`
|
||||||
|
Password string `yaml:"password"`
|
||||||
Skimmer bool `yaml:"skimmer"`
|
Skimmer bool `yaml:"skimmer"`
|
||||||
FT8 bool `yaml:"ft8"`
|
FT8 bool `yaml:"ft8"`
|
||||||
FT4 bool `yaml:"ft4"`
|
FT4 bool `yaml:"ft4"`
|
||||||
@ -44,6 +56,16 @@ type Config struct {
|
|||||||
Host string `yaml:"host"`
|
Host string `yaml:"host"`
|
||||||
Port string `yaml:"port"`
|
Port string `yaml:"port"`
|
||||||
} `yaml:"telnetserver"`
|
} `yaml:"telnetserver"`
|
||||||
|
|
||||||
|
Gotify struct {
|
||||||
|
Enable bool `yaml:"enable"`
|
||||||
|
URL string `yaml:"url"`
|
||||||
|
Token string `yaml:"token"`
|
||||||
|
NewDXCC bool `yaml:"NewDXCC"`
|
||||||
|
NewBand bool `yaml:"NewBand"`
|
||||||
|
NewMode bool `yaml:"NewMode"`
|
||||||
|
NewBandAndMode bool `yaml:"NewBandAndMode"`
|
||||||
|
} `yaml:"gotify"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig(configPath string) *Config {
|
func NewConfig(configPath string) *Config {
|
||||||
|
25
config.yml
25
config.yml
@ -1,21 +1,30 @@
|
|||||||
general:
|
general:
|
||||||
delete_log_file_at_start: true
|
delete_log_file_at_start: true
|
||||||
|
callsign: F4BPO # Log4OM Callsign used to check if you get spotted by someone
|
||||||
log_to_file: true
|
log_to_file: true
|
||||||
log_level: DEBUG # INFO or DEBUG or WARN
|
log_level: DEBUG # INFO or DEBUG or WARN
|
||||||
telnetserver: true # not in use for now
|
telnetserver: true # not in use for now
|
||||||
flexradiospot: true # not in use for now
|
flexradiospot: true # not in use for now
|
||||||
|
database:
|
||||||
|
mysql: true #only one of the two can be true
|
||||||
|
sqlite: false
|
||||||
|
mysql_db_user: rouggy
|
||||||
|
mysql_db_password: 89DGgg290379
|
||||||
|
mysql_db_name: log_f4bpo
|
||||||
|
mysql_host: 10.10.10.15
|
||||||
|
mysql_port: 3306
|
||||||
sqlite:
|
sqlite:
|
||||||
sqlite_path: 'C:\Perso\Seafile\Radio\Logs\Log4OM\F4BPO.SQLite' # SQLite Db oath of Log4OM
|
sqlite_path: 'C:\Perso\Seafile\Radio\Logs\Log4OM\F4BPO.SQLite' # SQLite Db oath of Log4OM
|
||||||
callsign: F4BPO # Log4OM Callsign used to check if you get spotted by someone
|
|
||||||
cluster:
|
cluster:
|
||||||
server: cluster.f4bpo.com # dxc.k0xm.net
|
server: cluster.f4bpo.com # dxc.k0xm.net dxc.sm7iun.se
|
||||||
port: 7300
|
port: 7300
|
||||||
login: f4bpo
|
login: f4bpo
|
||||||
|
password: 89DGgg
|
||||||
skimmer: true
|
skimmer: true
|
||||||
ft8: false
|
ft8: false
|
||||||
ft4: false
|
ft4: false
|
||||||
command: #SET/NOFILTER
|
command:
|
||||||
login_prompt: "Please enter your call:"
|
login_prompt: "login:"
|
||||||
flex:
|
flex:
|
||||||
discovery: true # Radio must be on same LAN than the program
|
discovery: true # Radio must be on same LAN than the program
|
||||||
ip: 10.10.10.120 # if discovery is true no need to put an IP
|
ip: 10.10.10.120 # if discovery is true no need to put an IP
|
||||||
@ -23,3 +32,11 @@ flex:
|
|||||||
telnetserver: # Log4OM must be connected to this server ie: localhost:7301 if on same machine as this program else ip:7301
|
telnetserver: # Log4OM must be connected to this server ie: localhost:7301 if on same machine as this program else ip:7301
|
||||||
host: 0.0.0.0
|
host: 0.0.0.0
|
||||||
port: 7301
|
port: 7301
|
||||||
|
gotify:
|
||||||
|
enable: false
|
||||||
|
url: https://gotify.rouggy.com/message
|
||||||
|
token: ALaGS4MVMWTEMcP
|
||||||
|
NewDXCC: true
|
||||||
|
NewBand: false
|
||||||
|
NewMode: false
|
||||||
|
NewBandAndMode: false
|
@ -6713,7 +6713,7 @@
|
|||||||
<CountryTag />
|
<CountryTag />
|
||||||
<CountryPrefixList>
|
<CountryPrefixList>
|
||||||
<CountryPrefix>
|
<CountryPrefix>
|
||||||
<PrefixList>^FO.*</PrefixList>
|
<PrefixList>^FO.*|TX5U</PrefixList>
|
||||||
<StartDate xsi:nil="true" />
|
<StartDate xsi:nil="true" />
|
||||||
<EndDate>1979-07-18T00:00:00Z</EndDate>
|
<EndDate>1979-07-18T00:00:00Z</EndDate>
|
||||||
</CountryPrefix>
|
</CountryPrefix>
|
||||||
@ -6723,7 +6723,7 @@
|
|||||||
<EndDate>1979-07-22T23:59:59Z</EndDate>
|
<EndDate>1979-07-22T23:59:59Z</EndDate>
|
||||||
</CountryPrefix>
|
</CountryPrefix>
|
||||||
<CountryPrefix>
|
<CountryPrefix>
|
||||||
<PrefixList>^FO.*</PrefixList>
|
<PrefixList>^FO.*|TX5U</PrefixList>
|
||||||
<StartDate>1979-07-22T23:59:59Z</StartDate>
|
<StartDate>1979-07-22T23:59:59Z</StartDate>
|
||||||
<EndDate xsi:nil="true" />
|
<EndDate xsi:nil="true" />
|
||||||
</CountryPrefix>
|
</CountryPrefix>
|
||||||
@ -12832,7 +12832,7 @@
|
|||||||
<EndDate>1979-12-31T23:59:59Z</EndDate>
|
<EndDate>1979-12-31T23:59:59Z</EndDate>
|
||||||
</CountryPrefix>
|
</CountryPrefix>
|
||||||
<CountryPrefix>
|
<CountryPrefix>
|
||||||
<PrefixList>^FR.*</PrefixList>
|
<PrefixList>^FR.*|^TO974REF</PrefixList>
|
||||||
<StartDate>1979-12-31T23:59:59Z</StartDate>
|
<StartDate>1979-12-31T23:59:59Z</StartDate>
|
||||||
<EndDate xsi:nil="true" />
|
<EndDate xsi:nil="true" />
|
||||||
</CountryPrefix>
|
</CountryPrefix>
|
||||||
|
93
database.go
93
database.go
@ -9,6 +9,8 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -37,18 +39,33 @@ type FlexDXClusterRepository struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewLog4OMContactsRepository(filePath string) *Log4OMContactsRepository {
|
func NewLog4OMContactsRepository(filePath string) *Log4OMContactsRepository {
|
||||||
db, err := sql.Open("sqlite3", filePath)
|
|
||||||
if err != nil {
|
if Cfg.Database.MySQL {
|
||||||
Log.Errorf("Cannot open db", err)
|
db, err := sql.Open("mysql", Cfg.Database.MySQLUser+":"+Cfg.Database.MySQLPassword+"@tcp("+Cfg.Database.MySQLHost+":"+Cfg.Database.MySQLPort+")/"+Cfg.Database.MySQLDbName)
|
||||||
}
|
if err != nil {
|
||||||
_, err = db.Exec("PRAGMA journal_mode=WAL")
|
Log.Errorf("Cannot open db", err)
|
||||||
if err != nil {
|
}
|
||||||
panic(err)
|
|
||||||
|
return &Log4OMContactsRepository{
|
||||||
|
db: db,
|
||||||
|
Log: Log}
|
||||||
|
|
||||||
|
} else if Cfg.Database.SQLite {
|
||||||
|
db, err := sql.Open("sqlite3", filePath)
|
||||||
|
if err != nil {
|
||||||
|
Log.Errorf("Cannot open db", err)
|
||||||
|
}
|
||||||
|
_, err = db.Exec("PRAGMA journal_mode=WAL")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Log4OMContactsRepository{
|
||||||
|
db: db,
|
||||||
|
Log: Log}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Log4OMContactsRepository{
|
return nil
|
||||||
db: db,
|
|
||||||
Log: Log}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFlexDXDatabase(filePath string) *FlexDXClusterRepository {
|
func NewFlexDXDatabase(filePath string) *FlexDXClusterRepository {
|
||||||
@ -95,6 +112,16 @@ func NewFlexDXDatabase(filePath string) *FlexDXClusterRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Log4OMContactsRepository) CountEntries() int {
|
||||||
|
var contacts int
|
||||||
|
_ = r.db.QueryRow("SELECT COUNT(*) FROM log").Scan(&contacts)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("could not query database", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return contacts
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Log4OMContactsRepository) ListByCountry(countryID string, contactsChan chan []Contact, wg *sync.WaitGroup) {
|
func (r *Log4OMContactsRepository) ListByCountry(countryID string, contactsChan chan []Contact, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE dxcc = ?", countryID)
|
rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE dxcc = ?", countryID)
|
||||||
@ -161,6 +188,52 @@ func (r *Log4OMContactsRepository) ListByCountryMode(countryID string, mode stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Log4OMContactsRepository) ListByCountryModeBand(countryID string, band string, mode string, contactsModeBandChan chan []Contact, wg *sync.WaitGroup) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
if mode == "USB" || mode == "LSB" {
|
||||||
|
|
||||||
|
rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE dxcc = ? AND (mode = ? OR mode = ?) AND band = ?", countryID, "USB", "LSB", band)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("could not query database", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
contacts := []Contact{}
|
||||||
|
for rows.Next() {
|
||||||
|
c := Contact{}
|
||||||
|
if err := rows.Scan(&c.Callsign, &c.Band, &c.Mode, &c.DXCC, &c.StationCallsign, &c.Country); err != nil {
|
||||||
|
log.Error("could not query database", err)
|
||||||
|
|
||||||
|
}
|
||||||
|
contacts = append(contacts, c)
|
||||||
|
}
|
||||||
|
contactsModeBandChan <- contacts
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE dxcc = ? AND mode = ? AND band = ?", countryID, mode, band)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("could not query the database", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
contacts := []Contact{}
|
||||||
|
for rows.Next() {
|
||||||
|
c := Contact{}
|
||||||
|
if err := rows.Scan(&c.Callsign, &c.Band, &c.Mode, &c.DXCC, &c.StationCallsign, &c.Country); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
|
||||||
|
}
|
||||||
|
contacts = append(contacts, c)
|
||||||
|
}
|
||||||
|
contactsModeBandChan <- contacts
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Log4OMContactsRepository) ListByCountryBand(countryID string, band string, contactsBandChan chan []Contact, wg *sync.WaitGroup) {
|
func (r *Log4OMContactsRepository) ListByCountryBand(countryID string, band string, contactsBandChan chan []Contact, wg *sync.WaitGroup) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE dxcc = ? AND band = ?", countryID, band)
|
rows, err := r.db.Query("SELECT callsign, band, mode, dxcc, stationcallsign, country FROM log WHERE dxcc = ? AND band = ?", countryID, band)
|
||||||
|
16
flexradio.go
16
flexradio.go
@ -34,6 +34,7 @@ type FlexSpot struct {
|
|||||||
NewDXCC bool
|
NewDXCC bool
|
||||||
NewBand bool
|
NewBand bool
|
||||||
NewMode bool
|
NewMode bool
|
||||||
|
NewSlot bool
|
||||||
Worked bool
|
Worked bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,6 +165,7 @@ func (fc *FlexClient) SendSpottoFlex(spot TelnetSpot) {
|
|||||||
NewDXCC: spot.NewDXCC,
|
NewDXCC: spot.NewDXCC,
|
||||||
NewBand: spot.NewBand,
|
NewBand: spot.NewBand,
|
||||||
NewMode: spot.NewMode,
|
NewMode: spot.NewMode,
|
||||||
|
NewSlot: spot.NewSlot,
|
||||||
Worked: spot.CallsignWorked,
|
Worked: spot.CallsignWorked,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +177,7 @@ func (fc *FlexClient) SendSpottoFlex(spot TelnetSpot) {
|
|||||||
flexSpot.Priority = "1"
|
flexSpot.Priority = "1"
|
||||||
flexSpot.BackgroundColor = "#ff000000"
|
flexSpot.BackgroundColor = "#ff000000"
|
||||||
flexSpot.Comment = flexSpot.Comment + " [New DXCC]"
|
flexSpot.Comment = flexSpot.Comment + " [New DXCC]"
|
||||||
} else if spot.DX == Cfg.SQLite.Callsign {
|
} else if spot.DX == Cfg.General.Callsign {
|
||||||
flexSpot.Color = "#ffff0000"
|
flexSpot.Color = "#ffff0000"
|
||||||
flexSpot.Priority = "1"
|
flexSpot.Priority = "1"
|
||||||
flexSpot.BackgroundColor = "#ff000000"
|
flexSpot.BackgroundColor = "#ff000000"
|
||||||
@ -199,6 +201,11 @@ func (fc *FlexClient) SendSpottoFlex(spot TelnetSpot) {
|
|||||||
flexSpot.Priority = "3"
|
flexSpot.Priority = "3"
|
||||||
flexSpot.BackgroundColor = "#ff000000"
|
flexSpot.BackgroundColor = "#ff000000"
|
||||||
flexSpot.Comment = flexSpot.Comment + " [New Band]"
|
flexSpot.Comment = flexSpot.Comment + " [New Band]"
|
||||||
|
} else if !spot.NewBand && !spot.NewMode && !spot.NewDXCC && !spot.CallsignWorked && spot.NewSlot {
|
||||||
|
flexSpot.Color = "#ff91d2ff"
|
||||||
|
flexSpot.Priority = "5"
|
||||||
|
flexSpot.BackgroundColor = "#ff000000"
|
||||||
|
flexSpot.Comment = flexSpot.Comment + " [New Slot]"
|
||||||
} else if !spot.NewBand && !spot.NewMode && !spot.NewDXCC && !spot.CallsignWorked {
|
} else if !spot.NewBand && !spot.NewMode && !spot.NewDXCC && !spot.CallsignWorked {
|
||||||
flexSpot.Color = "#ffeaeaea"
|
flexSpot.Color = "#ffeaeaea"
|
||||||
flexSpot.Priority = "5"
|
flexSpot.Priority = "5"
|
||||||
@ -209,6 +216,9 @@ func (fc *FlexClient) SendSpottoFlex(spot TelnetSpot) {
|
|||||||
flexSpot.BackgroundColor = "#ff000000"
|
flexSpot.BackgroundColor = "#ff000000"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send notification to Gotify
|
||||||
|
Gotify(flexSpot)
|
||||||
|
|
||||||
flexSpot.Comment = strings.ReplaceAll(flexSpot.Comment, " ", "\u00A0")
|
flexSpot.Comment = strings.ReplaceAll(flexSpot.Comment, " ", "\u00A0")
|
||||||
|
|
||||||
srcFlexSpot, err := fc.Repo.FindDXSameBand(flexSpot)
|
srcFlexSpot, err := fc.Repo.FindDXSameBand(flexSpot)
|
||||||
@ -276,10 +286,10 @@ func (fc *FlexClient) ReadLine() {
|
|||||||
Log.Errorf("could not find spot by flex spot number in database: %s", err)
|
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)
|
msg := fmt.Sprintf(`To ALL de %s <%s> : Clicked on "%s" at %s`, Cfg.General.Callsign, spot.UTCTime, spot.DX, spot.FrequencyHz)
|
||||||
if len(fc.TCPServer.Clients) > 0 {
|
if len(fc.TCPServer.Clients) > 0 {
|
||||||
fc.MsgChan <- msg
|
fc.MsgChan <- msg
|
||||||
Log.Infof("%s clicked on spot \"%s\" at %s", Cfg.SQLite.Callsign, spot.DX, spot.FrequencyMhz)
|
Log.Infof("%s clicked on spot \"%s\" at %s", Cfg.General.Callsign, spot.DX, spot.FrequencyMhz)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
2
go.mod
2
go.mod
@ -3,6 +3,7 @@ module git.rouggy.com/rouggy/FlexDXCluster
|
|||||||
go 1.23.1
|
go 1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3
|
||||||
github.com/mattn/go-sqlite3 v1.14.23
|
github.com/mattn/go-sqlite3 v1.14.23
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
||||||
@ -10,6 +11,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||||
|
4
go.sum
4
go.sum
@ -1,9 +1,13 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
86
gotify.go
Normal file
86
gotify.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GotifyMessage struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Gotify(spot FlexSpot) {
|
||||||
|
|
||||||
|
if Cfg.Gotify.Enable {
|
||||||
|
|
||||||
|
message := fmt.Sprintf("DX: %s\nFrom: %s\nFreq: %s\nMode: %s\n", spot.DX, spot.Source, spot.FrequencyMhz, spot.Mode)
|
||||||
|
|
||||||
|
gotifyMsg := GotifyMessage{
|
||||||
|
Title: "",
|
||||||
|
Message: message,
|
||||||
|
Priority: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
if spot.NewDXCC && Cfg.Gotify.NewDXCC {
|
||||||
|
title := "FlexDXCluster New DXCC"
|
||||||
|
gotifyMsg.Title = title
|
||||||
|
gotifyMsg.Message = message
|
||||||
|
sendToGotify(gotifyMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if spot.NewBand && spot.NewMode && Cfg.Gotify.NewBandAndMode {
|
||||||
|
title := "FlexDXCluster New Mode & Band"
|
||||||
|
gotifyMsg.Title = title
|
||||||
|
gotifyMsg.Message = message
|
||||||
|
sendToGotify(gotifyMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if spot.NewMode && Cfg.Gotify.NewMode && !spot.NewBand {
|
||||||
|
title := "FlexDXCluster New Mode"
|
||||||
|
gotifyMsg.Title = title
|
||||||
|
gotifyMsg.Message = message
|
||||||
|
sendToGotify(gotifyMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if spot.NewBand && Cfg.Gotify.NewBand && !spot.NewMode {
|
||||||
|
title := "FlexDXCluster New Band"
|
||||||
|
gotifyMsg.Title = title
|
||||||
|
gotifyMsg.Message = message
|
||||||
|
sendToGotify(gotifyMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendToGotify(mess GotifyMessage) {
|
||||||
|
jsonData, err := json.Marshal(mess)
|
||||||
|
if err != nil {
|
||||||
|
Log.Errorln("Error marshaling JSON:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", Cfg.Gotify.URL, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
Log.Errorln("Error creating request:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Add("Authorization", "Bearer "+Cfg.Gotify.Token)
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
Log.Errorln("Error sending request:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
Log.Errorln("Gotify server returned non-OK status:", resp.Status)
|
||||||
|
} else {
|
||||||
|
Log.Debugln("Push successfully sent to Gotify")
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 202 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.2 MiB |
11
main.go
11
main.go
@ -42,11 +42,16 @@ func main() {
|
|||||||
cfg := NewConfig(cfgPath)
|
cfg := NewConfig(cfgPath)
|
||||||
|
|
||||||
log := NewLog()
|
log := NewLog()
|
||||||
log.Info("Running FlexDXCluster version 0.2")
|
log.Info("Running FlexDXCluster version 0.6")
|
||||||
log.Infof("Callsign: %s", cfg.SQLite.Callsign)
|
log.Infof("Callsign: %s", cfg.General.Callsign)
|
||||||
|
|
||||||
DeleteDatabase("./flex.sqlite", log)
|
DeleteDatabase("./flex.sqlite", log)
|
||||||
|
|
||||||
|
log.Infof("Gotify Push Enabled: %v", cfg.Gotify.Enable)
|
||||||
|
if cfg.Gotify.Enable {
|
||||||
|
log.Infof("Gotify Push NewDXCC: %v - NewBand: %v - NewMode: %v - NewBandAndMode: %v", cfg.Gotify.NewDXCC, cfg.Gotify.NewBand, cfg.Gotify.NewMode, cfg.Gotify.NewBandAndMode)
|
||||||
|
}
|
||||||
|
|
||||||
// Load country.xml to get all the DXCC number
|
// Load country.xml to get all the DXCC number
|
||||||
Countries := LoadCountryFile()
|
Countries := LoadCountryFile()
|
||||||
|
|
||||||
@ -56,6 +61,8 @@ func main() {
|
|||||||
|
|
||||||
// Database connection to Log4OM
|
// Database connection to Log4OM
|
||||||
cRepo := NewLog4OMContactsRepository(cfg.SQLite.SQLitePath)
|
cRepo := NewLog4OMContactsRepository(cfg.SQLite.SQLitePath)
|
||||||
|
contacts := cRepo.CountEntries()
|
||||||
|
log.Infof("Log4OM Database Contains %v Contacts", contacts)
|
||||||
defer cRepo.db.Close()
|
defer cRepo.db.Close()
|
||||||
|
|
||||||
TCPServer := NewTCPServer(cfg.TelnetServer.Host, cfg.TelnetServer.Port)
|
TCPServer := NewTCPServer(cfg.TelnetServer.Host, cfg.TelnetServer.Port)
|
||||||
|
23
spot.go
23
spot.go
@ -23,10 +23,14 @@ type TelnetSpot struct {
|
|||||||
NewDXCC bool
|
NewDXCC bool
|
||||||
NewBand bool
|
NewBand bool
|
||||||
NewMode bool
|
NewMode bool
|
||||||
|
NewSlot bool
|
||||||
CallsignWorked bool
|
CallsignWorked bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// var spotNumber = 1
|
||||||
|
|
||||||
func ProcessTelnetSpot(re *regexp.Regexp, spotRaw string, SpotChanToFlex chan TelnetSpot, SpotChanToHTTPServer chan TelnetSpot, Countries Countries) {
|
func ProcessTelnetSpot(re *regexp.Regexp, spotRaw string, SpotChanToFlex chan TelnetSpot, SpotChanToHTTPServer chan TelnetSpot, Countries Countries) {
|
||||||
|
|
||||||
match := re.FindStringSubmatch(spotRaw)
|
match := re.FindStringSubmatch(spotRaw)
|
||||||
|
|
||||||
if len(match) != 0 {
|
if len(match) != 0 {
|
||||||
@ -52,17 +56,19 @@ func ProcessTelnetSpot(re *regexp.Regexp, spotRaw string, SpotChanToFlex chan Te
|
|||||||
spot.NewBand = false
|
spot.NewBand = false
|
||||||
spot.NewMode = false
|
spot.NewMode = false
|
||||||
spot.NewDXCC = false
|
spot.NewDXCC = false
|
||||||
|
spot.NewSlot = false
|
||||||
|
|
||||||
contactRepo := NewLog4OMContactsRepository(Cfg.SQLite.SQLitePath)
|
contactRepo := NewLog4OMContactsRepository(Cfg.SQLite.SQLitePath)
|
||||||
defer contactRepo.db.Close()
|
defer contactRepo.db.Close()
|
||||||
|
|
||||||
contactsChan := make(chan []Contact)
|
contactsChan := make(chan []Contact)
|
||||||
contactsModeChan := make(chan []Contact)
|
contactsModeChan := make(chan []Contact)
|
||||||
|
contactsModeBandChan := make(chan []Contact)
|
||||||
contactsBandChan := make(chan []Contact)
|
contactsBandChan := make(chan []Contact)
|
||||||
contactsCallChan := make(chan []Contact)
|
contactsCallChan := make(chan []Contact)
|
||||||
|
|
||||||
wg := new(sync.WaitGroup)
|
wg := new(sync.WaitGroup)
|
||||||
wg.Add(4)
|
wg.Add(5)
|
||||||
|
|
||||||
go contactRepo.ListByCountry(spot.DXCC, contactsChan, wg)
|
go contactRepo.ListByCountry(spot.DXCC, contactsChan, wg)
|
||||||
contacts := <-contactsChan
|
contacts := <-contactsChan
|
||||||
@ -76,6 +82,9 @@ func ProcessTelnetSpot(re *regexp.Regexp, spotRaw string, SpotChanToFlex chan Te
|
|||||||
go contactRepo.ListByCallSign(spot.DX, spot.Band, spot.Mode, contactsCallChan, wg)
|
go contactRepo.ListByCallSign(spot.DX, spot.Band, spot.Mode, contactsCallChan, wg)
|
||||||
contactsCall := <-contactsCallChan
|
contactsCall := <-contactsCallChan
|
||||||
|
|
||||||
|
go contactRepo.ListByCountryModeBand(spot.DXCC, spot.Band, spot.Mode, contactsModeBandChan, wg)
|
||||||
|
contactsModeBand := <-contactsModeBandChan
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
if len(contacts) == 0 {
|
if len(contacts) == 0 {
|
||||||
@ -88,6 +97,11 @@ func ProcessTelnetSpot(re *regexp.Regexp, spotRaw string, SpotChanToFlex chan Te
|
|||||||
if len(contactsBand) == 0 {
|
if len(contactsBand) == 0 {
|
||||||
spot.NewBand = true
|
spot.NewBand = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(contactsModeBand) == 0 {
|
||||||
|
spot.NewSlot = true
|
||||||
|
}
|
||||||
|
|
||||||
if len(contactsCall) > 0 {
|
if len(contactsCall) > 0 {
|
||||||
spot.CallsignWorked = true
|
spot.CallsignWorked = true
|
||||||
}
|
}
|
||||||
@ -115,6 +129,11 @@ func ProcessTelnetSpot(re *regexp.Regexp, spotRaw string, SpotChanToFlex chan Te
|
|||||||
spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC)
|
spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !spot.NewDXCC && !spot.NewBand && !spot.NewMode && spot.NewSlot && spot.Mode != "" {
|
||||||
|
Log.Debugf("(** New Slot **) DX: %s - Spotter: %s - Freq: %s - Band: %s - Mode: %s - Comment: %s - Time: %s - DXCC: %s",
|
||||||
|
spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC)
|
||||||
|
}
|
||||||
|
|
||||||
if !spot.NewDXCC && !spot.NewBand && !spot.NewMode && spot.CallsignWorked {
|
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",
|
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)
|
spot.DX, spot.Spotter, spot.Frequency, spot.Band, spot.Mode, spot.Comment, spot.Time, spot.DXCC)
|
||||||
@ -128,6 +147,8 @@ func ProcessTelnetSpot(re *regexp.Regexp, spotRaw string, SpotChanToFlex chan Te
|
|||||||
// Log.Infof("Could not decode: %s", strings.Trim(spotRaw, "\n"))
|
// Log.Infof("Could not decode: %s", strings.Trim(spotRaw, "\n"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log.Infof("Spots Processed: %v", spotNumber)
|
||||||
|
// spotNumber++
|
||||||
}
|
}
|
||||||
|
|
||||||
func (spot *TelnetSpot) GetBand() {
|
func (spot *TelnetSpot) GetBand() {
|
||||||
|
Reference in New Issue
Block a user