Added local compilation. Cleaning.
This commit is contained in:
parent
026c1aa3bb
commit
6a96656434
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,5 +1,4 @@
|
|||||||
out/
|
out/
|
||||||
Makefile
|
|
||||||
go.sum
|
go.sum
|
||||||
.git/
|
.git/
|
||||||
*.swp
|
*.swp
|
||||||
|
7
Makefile
Normal file
7
Makefile
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export GOOS=linux
|
||||||
|
export GOARCH=arm
|
||||||
|
export GOARM=6
|
||||||
|
export CGO_ENABLED=0
|
||||||
|
|
||||||
|
build:
|
||||||
|
@go build -o out/modem main.go
|
@ -13,7 +13,7 @@ import (
|
|||||||
// Some constants
|
// Some constants
|
||||||
const (
|
const (
|
||||||
ReadTimeout = time.Second
|
ReadTimeout = time.Second
|
||||||
InputBufSize = 128
|
InputBufSize = 2048
|
||||||
)
|
)
|
||||||
|
|
||||||
type atPort struct {
|
type atPort struct {
|
||||||
|
@ -18,8 +18,10 @@ type gps struct {
|
|||||||
type Gps interface {
|
type Gps interface {
|
||||||
Init() error
|
Init() error
|
||||||
Update() error
|
Update() error
|
||||||
|
|
||||||
GetData() Data
|
GetData() Data
|
||||||
CheckStatus() (Status, error)
|
GetStatus() (Status, error)
|
||||||
|
|
||||||
io.Closer
|
io.Closer
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +58,14 @@ func (g *gps) Update() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *gps) GetData() Data {
|
||||||
|
return g.data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *gps) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (g *gps) switchGpsMode(on bool) error {
|
func (g *gps) switchGpsMode(on bool) error {
|
||||||
onStr := "0"
|
onStr := "0"
|
||||||
if on {
|
if on {
|
||||||
@ -94,11 +104,3 @@ func (g *gps) switchGpsMode(on bool) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *gps) GetData() Data {
|
|
||||||
return g.data
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *gps) Close() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
@ -85,6 +85,9 @@ func (g *gps) rawCollect(flags nmeaFlags) (string, error) {
|
|||||||
if _, err := s.Write([]byte("AT+CGPSINFOCFG=0,31\r\n")); err != nil {
|
if _, err := s.Write([]byte("AT+CGPSINFOCFG=0,31\r\n")); err != nil {
|
||||||
return "", fmt.Errorf("serial port write 2: %w", err)
|
return "", fmt.Errorf("serial port write 2: %w", err)
|
||||||
}
|
}
|
||||||
|
if _, err := s.Write([]byte("AT+CGPSFTM=0\r\n")); err != nil { // For sure because sometimes it cannot stop...
|
||||||
|
return "", fmt.Errorf("serial port write 2: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond) // To enshure
|
time.Sleep(100 * time.Millisecond) // To enshure
|
||||||
|
|
||||||
@ -115,7 +118,7 @@ func (g *gps) collectNmeaReports(flags nmeaFlags) ([]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG
|
// DEBUG
|
||||||
g.logger.Println("NMEA raw collect:", resp)
|
// g.logger.Println("NMEA raw collect:", resp)
|
||||||
|
|
||||||
// Right responce struct:
|
// Right responce struct:
|
||||||
// \r\n
|
// \r\n
|
||||||
|
@ -12,24 +12,13 @@ type Status struct {
|
|||||||
ActiveSatelitesCounte int `json:"activeSatelitesCount"`
|
ActiveSatelitesCounte int `json:"activeSatelitesCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st Status) String() string {
|
|
||||||
got := "false"
|
|
||||||
if st.GotResponses {
|
|
||||||
got = "true"
|
|
||||||
}
|
|
||||||
return "" +
|
|
||||||
"GotResponses: " + got + "\n" +
|
|
||||||
"FoundSatelitesCount: " + string(st.FoundSatelitesCount) + "\n" +
|
|
||||||
"ActiveSatelitesCounte: " + string(st.ActiveSatelitesCounte) + "\n"
|
|
||||||
}
|
|
||||||
|
|
||||||
var StatusNil = Status{
|
var StatusNil = Status{
|
||||||
GotResponses: false,
|
GotResponses: false,
|
||||||
FoundSatelitesCount: 0,
|
FoundSatelitesCount: 0,
|
||||||
ActiveSatelitesCounte: 0,
|
ActiveSatelitesCounte: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *gps) CheckStatus() (Status, error) {
|
func (g *gps) GetStatus() (Status, error) {
|
||||||
// Provides more information about signal and possible problems using NMEA reports
|
// Provides more information about signal and possible problems using NMEA reports
|
||||||
|
|
||||||
// Collect reports
|
// Collect reports
|
@ -26,13 +26,12 @@ type modem struct {
|
|||||||
logger *log.Logger
|
logger *log.Logger
|
||||||
|
|
||||||
// Serial values
|
// Serial values
|
||||||
baudrate int
|
baudrate int
|
||||||
|
|
||||||
deviceName string
|
deviceName string
|
||||||
port at.Port
|
port at.Port
|
||||||
|
|
||||||
// Gpio values
|
// Gpio values
|
||||||
onOffPin gpio.Pin
|
onOffPin gpio.Pin // For turning on and off
|
||||||
|
|
||||||
// Other values
|
// Other values
|
||||||
lastUpdateTime time.Time
|
lastUpdateTime time.Time
|
||||||
@ -44,19 +43,19 @@ type modem struct {
|
|||||||
ic internet.Conn
|
ic internet.Conn
|
||||||
|
|
||||||
// Sms and calls
|
// Sms and calls
|
||||||
sms sms.Dialer
|
sms sms.Sms
|
||||||
}
|
}
|
||||||
|
|
||||||
type Modem interface {
|
type Modem interface {
|
||||||
Init() error
|
Init() error
|
||||||
Validate() bool
|
IsConnected() bool
|
||||||
Update() error
|
Update() error
|
||||||
GetData() ModemData
|
GetData() ModemData
|
||||||
|
|
||||||
// Temp access to SMS, GPS, AT interfaces mostly for debug
|
// Access to SMS, GPS, AT interfaces mostly for debug
|
||||||
At() at.Port
|
At() at.Port // Send
|
||||||
Gps() gps.Gps
|
Gps() gps.Gps // Update, GetData, GetStatus
|
||||||
Sms() sms.Dialer
|
Sms() sms.Sms // Send, ReadNew
|
||||||
|
|
||||||
io.Closer
|
io.Closer
|
||||||
}
|
}
|
||||||
@ -77,7 +76,8 @@ func (m *modem) Init() error {
|
|||||||
if err := m.onOffPin.Init(); err != nil {
|
if err := m.onOffPin.Init(); err != nil {
|
||||||
return fmt.Errorf("gpio pin init: %w", err)
|
return fmt.Errorf("gpio pin init: %w", err)
|
||||||
}
|
}
|
||||||
// m.onOffPin.PowerOn()
|
m.logger.Println("TURNING ON IS COMMENTED NOW!!!")
|
||||||
|
// m.onOffPin.PowerOn() // DEBUG do not want to wait 30 seconds
|
||||||
|
|
||||||
// Search
|
// Search
|
||||||
m.logger.Println("=============================== Search")
|
m.logger.Println("=============================== Search")
|
||||||
@ -101,40 +101,44 @@ func (m *modem) Init() error {
|
|||||||
return fmt.Errorf("connect: %w", err)
|
return fmt.Errorf("connect: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Establish internet connection
|
|
||||||
// m.logger.Println("=============================== Internet")
|
|
||||||
// m.ic = internet.New(log.New(m.logger.Writer(), "internet", log.LstdFlags), m.port)
|
|
||||||
// if err := m.ic.Init(); err != nil {
|
|
||||||
// return fmt.Errorf("internet connection init: %w", err)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Init submodules
|
// Init submodules
|
||||||
|
// submodulesLogger := m.logger.Writer() // FOR more logs
|
||||||
|
submodulesLogger := io.Discard // FOR less logs
|
||||||
|
|
||||||
m.logger.Println("=============================== Init submodules")
|
m.logger.Println("=============================== Init submodules")
|
||||||
m.sms = sms.New(log.New(m.logger.Writer(), "modem-sms", log.LstdFlags), m.port)
|
m.ic = internet.New(log.New(submodulesLogger, "modem-internet : ", log.LstdFlags), m.port)
|
||||||
|
if err := m.ic.Init(); err != nil {
|
||||||
|
m.logger.Printf("\x1b[38;2;255;0;0mInternet: %s\x1b[38;2;255;255;255m\n", err.Error())
|
||||||
|
} else {
|
||||||
|
m.logger.Println("\x1b[38;2;0;255;0mInternet OK\x1b[38;2;255;255;255m")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.sms = sms.New(log.New(submodulesLogger, "modem-sms : ", log.LstdFlags), m.port)
|
||||||
if err := m.sms.Init(); err != nil {
|
if err := m.sms.Init(); err != nil {
|
||||||
return fmt.Errorf("sms dialer init %w", err)
|
m.logger.Printf("\x1b[38;2;255;0;0mSMS: %s\x1b[38;2;255;255;255m\n", err.Error())
|
||||||
|
} else {
|
||||||
|
m.logger.Println("\x1b[38;2;0;255;0mSMS OK\x1b[38;2;255;255;255m")
|
||||||
}
|
}
|
||||||
m.gps = gps.New(log.New(m.logger.Writer(), "modem-gps", log.LstdFlags), m.port)
|
|
||||||
|
m.gps = gps.New(log.New(submodulesLogger, "modem-gps : ", log.LstdFlags), m.port)
|
||||||
if err := m.gps.Init(); err != nil {
|
if err := m.gps.Init(); err != nil {
|
||||||
return fmt.Errorf("gps init %w", err)
|
m.logger.Printf("\x1b[38;2;255;0;0mgps init %w\x1b[38;2;255;255;255m\n", err)
|
||||||
|
} else {
|
||||||
|
m.logger.Println("\x1b[38;2;0;255;0mGPS OK\x1b[38;2;255;255;255m")
|
||||||
}
|
}
|
||||||
m.logger.Println(m.gps, m)
|
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
m.logger.Println("=============================== Test")
|
// GPS works fine but almost always there is no signal
|
||||||
if err := m.testGPS(); err != nil {
|
// m.logger.Println("=============================== Test")
|
||||||
return fmt.Errorf("testGPS: %w", err)
|
// if err := m.testGPS(); err != nil {
|
||||||
}
|
// return fmt.Errorf("testGPS: %w", err)
|
||||||
|
// }
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modem) Validate() bool {
|
|
||||||
return m.isConnected()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *modem) Update() error {
|
func (m *modem) Update() error {
|
||||||
if !m.isConnected() {
|
if !m.IsConnected() {
|
||||||
m.logger.Println("No connection to module")
|
m.logger.Println("No connection to module")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -142,6 +146,8 @@ func (m *modem) Update() error {
|
|||||||
if err := m.gps.Update(); err != nil {
|
if err := m.gps.Update(); err != nil {
|
||||||
m.logger.Println("gps update:", err.Error())
|
m.logger.Println("gps update:", err.Error())
|
||||||
}
|
}
|
||||||
|
// Read new messages
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +158,7 @@ func (m *modem) GetData() ModemData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modem) Sms() sms.Dialer {
|
func (m *modem) Sms() sms.Sms {
|
||||||
return m.sms
|
return m.sms
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,25 +170,26 @@ func (m *modem) At() at.Port {
|
|||||||
return m.port
|
return m.port
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modem) Close() error {
|
func (m *modem) Close() error { // I can't return error so I log it
|
||||||
if err := m.sms.Close(); err != nil {
|
if err := m.sms.Close(); err != nil {
|
||||||
return fmt.Errorf("sms: %w", err)
|
m.logger.Printf("\x1b[38;2;255;0;0mclose sms error: %s\x1b[38;2;255;255;255m\n", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not right way I think
|
|
||||||
if err := m.port.Close(); err != nil {
|
if err := m.port.Close(); err != nil {
|
||||||
return fmt.Errorf("serial port: %w", err)
|
m.logger.Printf("\x1b[38;2;255;0;0mclose serial port error: %s\x1b[38;2;255;255;255m\n", err.Error())
|
||||||
}
|
}
|
||||||
if err := m.onOffPin.Close(); err != nil {
|
if err := m.onOffPin.Close(); err != nil {
|
||||||
return fmt.Errorf("gpio pin: %w", err)
|
m.logger.Printf("\x1b[38;2;255;0;0mclose gpio pin error: %s\x1b[38;2;255;255;255m\n", err.Error())
|
||||||
}
|
}
|
||||||
if err := m.ic.Close(); err != nil {
|
if err := m.ic.Close(); err != nil {
|
||||||
return fmt.Errorf("internet connection: %w", err)
|
m.logger.Printf("\x1b[38;2;255;0;0mclose internet connection error: %s\x1b[38;2;255;255;255m\n", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////// Private functions
|
||||||
|
|
||||||
func (m *modem) connect() error {
|
func (m *modem) connect() error {
|
||||||
if m.port == nil {
|
if m.port == nil {
|
||||||
return fmt.Errorf("port is not defined")
|
return fmt.Errorf("port is not defined")
|
||||||
@ -197,7 +204,7 @@ func (m *modem) disconnect() error {
|
|||||||
return m.port.Disconnect()
|
return m.port.Disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modem) isConnected() bool {
|
func (m *modem) IsConnected() bool {
|
||||||
if m.port != nil {
|
if m.port != nil {
|
||||||
return m.port.IsConnected()
|
return m.port.IsConnected()
|
||||||
}
|
}
|
||||||
@ -234,22 +241,17 @@ func (m *modem) saveGPS(path string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modem) testConsole() {
|
// Short way to send command
|
||||||
for {
|
func (m *modem) printCmd(cmd string) {
|
||||||
var inStr string
|
if resp, err := m.port.Send(cmd); err != nil {
|
||||||
fmt.Scanln(&inStr)
|
m.logger.Println("FAILED TO SEND REQ", cmd, ":", err.Error())
|
||||||
if inStr == "exit" {
|
} else {
|
||||||
return
|
_ = resp
|
||||||
}
|
// m.logger.Println("CMD", cmd, ":", resp)
|
||||||
resp, err := m.port.Send(inStr)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Println("ERROR:", err.Error())
|
|
||||||
}
|
|
||||||
m.logger.Println(resp)
|
|
||||||
m.logger.Println("------------------")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Some required commands before checking port
|
||||||
func (m *modem) setupPort() error {
|
func (m *modem) setupPort() error {
|
||||||
// Reset input
|
// Reset input
|
||||||
if err := m.port.SerialPort().ResetInputBuffer(); err != nil {
|
if err := m.port.SerialPort().ResetInputBuffer(); err != nil {
|
||||||
@ -259,37 +261,11 @@ func (m *modem) setupPort() error {
|
|||||||
if err := m.port.SerialPort().ResetOutputBuffer(); err != nil {
|
if err := m.port.SerialPort().ResetOutputBuffer(); err != nil {
|
||||||
return fmt.Errorf("reset output buffer: %w", err)
|
return fmt.Errorf("reset output buffer: %w", err)
|
||||||
}
|
}
|
||||||
m.port.Send("ATE0") // This shit sometimes enables echo mode... why... when... but it can
|
|
||||||
|
|
||||||
// Turn on errors' describtion
|
// These commands ensure that correct modes are set
|
||||||
if resp, err := m.port.Send("AT+CMEE=2"); err != nil || !resp.Check() {
|
m.printCmd("ATE0") // Sometimes enables echo mode
|
||||||
if err != nil {
|
m.printCmd("AT+CGPSFTM=0") // Sometimes does not turn off nmea
|
||||||
return fmt.Errorf("AT+CMEE=2 request: %w", err)
|
m.printCmd("AT+CMEE=2") // Turn on errors describtion
|
||||||
}
|
|
||||||
return fmt.Errorf("turn on errors: error response: %s", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display other values
|
|
||||||
m.logger.Println("DEBUG SIM VALUES:")
|
|
||||||
// ICCID
|
|
||||||
if resp, err := m.port.Send("AT+CCID"); err != nil || !resp.Check() {
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("AT+CCID request: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("get ICCID: error response: %s", resp)
|
|
||||||
} else {
|
|
||||||
m.logger.Println("ICCID:", strings.Split(resp.String(), "\n")[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// ICCID
|
|
||||||
if resp, err := m.port.Send("AT+CNUM"); err != nil || !resp.Check() {
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("AT+CNUM request: %w", err)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("get CNUM: error response: %s", resp)
|
|
||||||
} else {
|
|
||||||
m.logger.Println("CNUM:", strings.Split(resp.String(), "\n")[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -14,14 +14,15 @@ type dialer struct {
|
|||||||
port at.Port
|
port at.Port
|
||||||
}
|
}
|
||||||
|
|
||||||
type Dialer interface {
|
type Sms interface {
|
||||||
Init() error
|
Init() error
|
||||||
Send(number, msg string) error
|
Send(number, msg string) error // Send sms
|
||||||
ReadNew() ([]string, error)
|
ReadNew() ([]string, error) // Read new smses
|
||||||
|
|
||||||
io.Closer
|
io.Closer
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(logger *log.Logger, port at.Port) Dialer {
|
func New(logger *log.Logger, port at.Port) Sms {
|
||||||
return &dialer{
|
return &dialer{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
port: port,
|
port: port,
|
||||||
@ -45,13 +46,15 @@ func (d *dialer) Init() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *dialer) Send(number, msg string) error {
|
func (d *dialer) Send(number, msg string) error {
|
||||||
d.port.Send(fmt.Sprintf("AT+CMGS=\"%s\"", number)) // Because it will throw error
|
d.port.Send(fmt.Sprintf(`AT+CMGS="%s"`, number)) // Because it will throw error
|
||||||
resp, err := d.port.RawSend(fmt.Sprintf("%s\x1A", msg)) // Add additional \r\n because there is not supposed to be
|
resp, err := d.port.RawSend(fmt.Sprintf("%s\n\r", msg)) // Add additional \r\n because there is not supposed to be
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("message request: %w", err)
|
return fmt.Errorf("message request: %w", err)
|
||||||
}
|
}
|
||||||
if at.Resp(resp).Check() {
|
d.logger.Println("SEND RESPONSE:", resp)
|
||||||
return nil
|
resp, err = d.port.RawSend("\x1A")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("message request: %w", err)
|
||||||
}
|
}
|
||||||
d.logger.Println("SEND RESPONSE:", resp)
|
d.logger.Println("SEND RESPONSE:", resp)
|
||||||
errCode, err := GetError([]byte(resp))
|
errCode, err := GetError([]byte(resp))
|
||||||
|
14
main.go
14
main.go
@ -16,18 +16,18 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mainE() error {
|
func mainE() error {
|
||||||
logger := log.New(os.Stdout, "main:", log.LstdFlags)
|
logger := log.New(os.Stdout, "main : ", log.LstdFlags)
|
||||||
m := modem.New(log.New(logger.Writer(), "modem:", log.LstdFlags))
|
m := modem.New(log.New(logger.Writer(), "modem : ", log.LstdFlags))
|
||||||
logger.Println("||||||||||||||||| INIT |||||||||||||||")
|
logger.Println("||||||||||||||||| INIT |||||||||||||||")
|
||||||
if err := m.Init(); err != nil {
|
if err := m.Init(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !m.Validate() {
|
if !m.IsConnected() {
|
||||||
logger.Println("AAAAAAAAAAAAAAA Validation failed")
|
logger.Println("AAAAAAAAAAAAAAA Modem is not connected")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
logger.Println("||||||||||||||||| GET INFO |||||||||||||||||")
|
logger.Println("||||||||||||||||| GET INFO |||||||||||||||||")
|
||||||
logger.Println(m.GetData())
|
logger.Printf("DATA: %+v\n", m.GetData())
|
||||||
|
|
||||||
// logger.Println("||||||||||||||||| SEND SMS |||||||||||||||||")
|
// logger.Println("||||||||||||||||| SEND SMS |||||||||||||||||")
|
||||||
// logger.Println(m.At().Send("AT+CNUM"))
|
// logger.Println(m.At().Send("AT+CNUM"))
|
||||||
@ -40,11 +40,11 @@ func mainE() error {
|
|||||||
// logger.Println("NEW:", ms)
|
// logger.Println("NEW:", ms)
|
||||||
// }
|
// }
|
||||||
logger.Println("||||||||||||||||| Checking gps status |||||||||||||||||")
|
logger.Println("||||||||||||||||| Checking gps status |||||||||||||||||")
|
||||||
st, err := m.Gps().CheckStatus()
|
st, err := m.Gps().GetStatus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
logger.Println("GPS Status:\n", st)
|
logger.Printf("GPS Status:%+v\n", st)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user