25 Commits

Author SHA1 Message Date
4f89f15146 Fix: latitude, longitude format 2024-09-30 20:43:37 +03:00
509006fae1 Add: power on with context, SIM808 support 2024-08-19 17:07:46 +03:00
3f5412fac0 Add: internet default routes 2024-08-15 16:47:20 +03:00
5065cfdfca Added GetTime. Added independent internet port. Debugged internet connection. 2024-08-12 19:24:31 +03:00
5059814d39 Added service checks. Some refactoring. 2024-08-12 13:40:26 +03:00
d21ad0ce00 Added signal check. 2024-08-10 15:21:27 +03:00
75d05f197c Improved SMS. 2024-08-09 18:17:20 +03:00
b3fb13b7b3 Removed internet and sms temporarily. 2024-08-09 11:42:55 +03:00
dde1411b18 Fixed RmFront panic. 2024-08-09 11:21:02 +03:00
6c110b9a8b Add: check in RmFront 2024-08-08 15:56:23 +03:00
cb07b6ac62 Fixed GPS info parsing. 2024-08-08 15:52:52 +03:00
be99ed7fbf Fix: import statements 2024-08-08 13:32:55 +03:00
d625866d92 Fix: import statements 2024-08-08 13:26:33 +03:00
7ee03906f8 Fix: go mod (AL1 told to) 2024-08-08 09:56:32 +00:00
0b76884112 Added RMS. 2024-08-07 17:34:56 +03:00
59bd2c5db3 Improve gps status. 2024-08-07 11:35:06 +03:00
0837d376f7 Added sync. 2024-08-06 21:10:24 +03:00
6a96656434 Added local compilation. Cleaning. 2024-08-06 20:37:20 +03:00
026c1aa3bb Fixed error logging. Added more logging of SIM info. 2024-08-04 15:59:58 +03:00
90a06e6afa Added GPS status. 2024-08-02 19:43:15 +03:00
e2e02ebbfe Fixed NMEA report parsing. 2024-08-02 19:16:39 +03:00
9bc94bfe7c Debugged collecting nmea data. 2024-08-02 18:32:33 +03:00
061ba2a859 Some debug. 2024-08-01 19:34:58 +03:00
6498d20378 Added draft GPS checks though NMEA reports. 2024-07-31 22:18:44 +03:00
b831d44294 Refactored gps. Prepared for adding advanced status checks. 2024-07-31 14:51:29 +03:00
22 changed files with 1791 additions and 618 deletions

1
.gitignore vendored
View File

@ -1,5 +1,4 @@
out/ out/
Makefile
go.sum go.sum
.git/ .git/
*.swp *.swp

7
Makefile Normal file
View File

@ -0,0 +1,7 @@
export GOOS=linux
export GOARCH=arm
export GOARM=6
export CGO_ENABLED=0
build:
@go build -o out/out main.go

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io" "io"
"log" "log"
"sync"
"time" "time"
"go.bug.st/serial" "go.bug.st/serial"
@ -12,12 +13,14 @@ import (
// Some constants // Some constants
const ( const (
ReadTimeout = time.Second ReadTimeout = time.Second
InputBufSize = 128 InputBufSize = 512
) )
type atPort struct { type atPort struct {
logger *log.Logger logger *log.Logger
mutex sync.Mutex // Mutex for all operation with serial port
baudrate int baudrate int
portName string portName string
port serial.Port port serial.Port
@ -27,14 +30,17 @@ type atPort struct {
type Port interface { type Port interface {
GetName() string GetName() string
GetBaudrate() int GetBaudrate() int
GetSerialPort() serial.Port // For extra need SerialPort() serial.Port // For extra need
Mutex() sync.Locker // retruns pointer to mutex for advanced use like readign NMEA reports
Connect() error Connect() error
Disconnect() error Disconnect() error
IsConnected() bool IsConnected() bool
RawSend(msg string) (string, error) RawSend(msg string) error
RawRead(timeout time.Duration) (string, error)
Send(cmd string) (Resp, error) Send(cmd string) (Resp, error)
SendWithTimeout(cmd string, timeout time.Duration) (Resp, error)
io.Closer io.Closer
} }
@ -56,11 +62,18 @@ func (p *atPort) GetBaudrate() int {
return p.baudrate return p.baudrate
} }
func (p *atPort) GetSerialPort() serial.Port { func (p *atPort) SerialPort() serial.Port {
return p.port return p.port
} }
func (p *atPort) Mutex() sync.Locker {
return &p.mutex
}
func (p *atPort) Connect() error { func (p *atPort) Connect() error {
p.mutex.Lock()
defer p.mutex.Unlock()
p.logger.Println("Connecting to", p.portName, "...") p.logger.Println("Connecting to", p.portName, "...")
s, err := serial.Open(p.portName, &serial.Mode{BaudRate: p.baudrate}) s, err := serial.Open(p.portName, &serial.Mode{BaudRate: p.baudrate})
if err != nil { if err != nil {
@ -76,8 +89,10 @@ func (p *atPort) Connect() error {
} }
func (p *atPort) Disconnect() error { func (p *atPort) Disconnect() error {
p.mutex.Lock()
defer func() { defer func() {
p.port = nil p.port = nil
p.mutex.Unlock()
}() }()
if err := p.port.Close(); err != nil { if err := p.port.Close(); err != nil {
return fmt.Errorf("close port: %w", err) return fmt.Errorf("close port: %w", err)
@ -86,34 +101,79 @@ func (p *atPort) Disconnect() error {
} }
func (p *atPort) IsConnected() bool { func (p *atPort) IsConnected() bool {
p.mutex.Lock()
defer p.mutex.Unlock()
return p.port != nil return p.port != nil
} }
func (p *atPort) RawRead(timeout time.Duration) (string, error) {
p.mutex.Lock()
defer p.mutex.Unlock()
deadline := time.Now().Add(timeout)
outBuf := make([]byte, 0)
readLoop:
for {
readLen, err := p.port.Read(p.inputBuf)
if err != nil {
return "", fmt.Errorf("port read: %w", err)
}
if readLen == 0 && time.Now().After(deadline) {
break readLoop
}
outBuf = append(outBuf, p.inputBuf[:readLen]...)
// if readLen < len(p.inputBuf) {
// break readLoop
// }
}
// p.logger.Println(msg, "\x1b[38;2;150;150;150mRAWREAD:", string(outBuf), "\x1b[38;2;255;255;255m")
return string(outBuf), nil
}
// Low level write/read function // Low level write/read function
func (p *atPort) RawSend(msg string) (string, error) { func (p *atPort) RawSend(msg string) error {
p.mutex.Lock()
defer p.mutex.Unlock()
// Write // Write
if _, err := p.port.Write([]byte(msg)); err != nil { if _, err := p.port.Write([]byte(msg)); err != nil {
return "", fmt.Errorf("serial port write: %w", err) return fmt.Errorf("serial port write: %w", err)
} }
time.Sleep(time.Millisecond) return nil
// Read
readLen, err := p.port.Read(p.inputBuf)
// p.logger.Println(msg, "\x1b[38;2;150;150;150mRAWREAD:", string(p.inputBuf[:readLen]), "\x1b[38;2;255;255;255m")
if err != nil {
return "", fmt.Errorf("port read: %w", err)
}
return string(p.inputBuf[:readLen]), nil
} }
func (p *atPort) Send(cmd string) (Resp, error) { func (p *atPort) Send(cmd string) (Resp, error) {
cmd += "\r\n" err := p.RawSend(cmd + "\r\n")
rawResp, err := p.RawSend(cmd)
if err != nil { if err != nil {
return RespNil, fmt.Errorf("make request: %w", err) return RespNil, fmt.Errorf("%s request: %w", cmd, err)
} }
rawResp, err := p.RawRead(ReadTimeout)
if err != nil {
return RespNil, fmt.Errorf("%s request: %w", cmd, err)
}
if len(rawResp) <= 4 { if len(rawResp) <= 4 {
return RespNil, fmt.Errorf("read too small msg: %d byte - %s", len(rawResp), string(rawResp)) return RespNil, fmt.Errorf("%s request: read too small msg: %d byte - %s", cmd, len(rawResp), string(rawResp))
}
resp := rawResp[2 : len(rawResp)-2] // Cut \r\n
return Resp(resp), nil
}
func (p *atPort) SendWithTimeout(cmd string, timeout time.Duration) (Resp, error) {
err := p.RawSend(cmd + "\r\n")
if err != nil {
return RespNil, fmt.Errorf("%s request: %w", cmd, err)
}
rawResp, err := p.RawRead(timeout)
if err != nil {
return RespNil, fmt.Errorf("%s request: %w", cmd, err)
}
if len(rawResp) <= 4 {
return RespNil, fmt.Errorf("%s request: read too small msg: %d byte - %s", cmd, len(rawResp), string(rawResp))
} }
resp := rawResp[2 : len(rawResp)-2] // Cut \r\n resp := rawResp[2 : len(rawResp)-2] // Cut \r\n
@ -121,5 +181,8 @@ func (p *atPort) Send(cmd string) (Resp, error) {
} }
func (p *atPort) Close() error { func (p *atPort) Close() error {
p.mutex.Lock()
defer p.mutex.Unlock()
return p.port.Close() return p.port.Close()
} }

View File

@ -12,6 +12,10 @@ func (resp Resp) RmFront(str string) Resp {
return Resp(string(resp)[len(str):]) return Resp(string(resp)[len(str):])
} }
func (resp Resp) CheckFront(str string) bool {
return len(resp) >= len(str) && resp[:len(str)].String() == str
}
func (resp Resp) String() string { func (resp Resp) String() string {
return string(resp) return string(resp)
} }

View File

@ -1,6 +1,7 @@
package gpio package gpio
import ( import (
"context"
"io" "io"
"log" "log"
"time" "time"
@ -8,6 +9,10 @@ import (
gpio "github.com/stianeikeland/go-rpio/v4" gpio "github.com/stianeikeland/go-rpio/v4"
) )
const (
waitCtxTimeout = 100 * time.Microsecond
)
type gpioPin struct { type gpioPin struct {
logger *log.Logger logger *log.Logger
pin gpio.Pin pin gpio.Pin
@ -16,7 +21,7 @@ type gpioPin struct {
type Pin interface { type Pin interface {
Init() error Init() error
PowerOn() PowerOn()
PowerOff() PowerOnCtx(ctx context.Context)
io.Closer io.Closer
} }
@ -31,32 +36,47 @@ func (p gpioPin) Init() error {
return gpio.Open() return gpio.Open()
} }
func (p gpioPin) sendOnOffSignal() { func waitCtx(ctx context.Context, timeout time.Duration) {
deadline := time.Now().Add(timeout)
for {
select {
case <-ctx.Done():
return
default:
if time.Now().After(deadline) {
return
}
}
time.Sleep(waitCtxTimeout)
}
}
func (p gpioPin) sendOnOffSignal(ctx context.Context) {
p.pin.Output() p.pin.Output()
p.logger.Println("Power on 0/3 + 100ms") p.logger.Println("Power on 0/3 + 100ms")
p.pin.Low() p.pin.Low()
p.pin.Toggle() p.pin.Toggle()
time.Sleep(100 * time.Millisecond) waitCtx(ctx, 100*time.Millisecond)
p.logger.Println("Power on 1/3 + 3s") p.logger.Println("Power on 1/3 + 3s")
p.pin.High() p.pin.High()
p.pin.Toggle() p.pin.Toggle()
time.Sleep(3 * time.Second) waitCtx(ctx, 3*time.Second)
p.logger.Println("Power on 2/3 + 30s") p.logger.Println("Power on 2/3 + 30s")
p.pin.Low() p.pin.Low()
p.pin.Toggle() p.pin.Toggle()
time.Sleep(30 * time.Second) waitCtx(ctx, 30*time.Second)
p.logger.Println("Power on 3/3") p.logger.Println("Power on 3/3")
} }
func (p gpioPin) PowerOn() { func (p gpioPin) PowerOn() {
p.sendOnOffSignal() p.sendOnOffSignal(context.Background())
} }
func (p gpioPin) PowerOff() { func (p gpioPin) PowerOnCtx(ctx context.Context) {
p.sendOnOffSignal() p.sendOnOffSignal(ctx)
} }
func (p gpioPin) Close() error { func (p gpioPin) Close() error {

View File

@ -1,137 +0,0 @@
package modem
import (
"fmt"
"math"
"strconv"
"strings"
"time"
"github.com/CGSG-2021-AE4/modem-test/api/modem/at"
)
type GpsData struct {
Latitude float64 `json:"Latitude"`
Longitude float64 `json:"Longitude"`
LatitudeIndicator string `json:"Latitude_indicator"` // North/South
LongitudeIndicator string `json:"Longitude_indicator"` // West/East
Speed float64 `json:"Speed"`
Course float64 `json:"-"`
Altitude float64 `json:"-"`
Date string `json:"-"`
Time string `json:"-"`
}
var GpsInfoNil = GpsData{}
func deg2rad(deg float64) float64 {
return deg * (math.Pi / 180)
}
func (gps *GpsData) calculateSpeed(newLatitude, newLongitude float64, lastUpdateTime time.Time) {
earthRad := 6371.0 // TODO ?
dLat := deg2rad(math.Abs(newLatitude - gps.Latitude))
dLon := deg2rad(math.Abs(newLongitude - gps.Longitude))
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(deg2rad(newLatitude))*math.Cos(deg2rad(gps.Latitude))*math.Sin(dLon/2)*math.Sin(dLon/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
gps.Speed = earthRad * c / (math.Abs(float64(time.Since(lastUpdateTime))))
}
// Parse string from AT command that contains gps data
func (gps *GpsData) decode(str string) error {
var err error
newGpsInfo := GpsData{}
strs := strings.Split(strings.Split(strings.Replace(str, " ", "", -1), "\n")[0], ",")
newGpsInfo.Latitude, err = strconv.ParseFloat(strs[0], 64)
if err != nil {
return fmt.Errorf("parse latitude: %w", err)
}
newGpsInfo.Longitude, err = strconv.ParseFloat(strs[2], 64)
if err != nil {
return fmt.Errorf("parse longitude: %w", err)
}
newGpsInfo.Latitude /= 100
newGpsInfo.Longitude /= 100
newGpsInfo.LatitudeIndicator = strs[1]
newGpsInfo.LongitudeIndicator = strs[3]
newGpsInfo.Date = strs[4]
newGpsInfo.Time = strs[5]
newGpsInfo.Altitude, err = strconv.ParseFloat(strs[6], 64)
if err != nil {
return fmt.Errorf("parse altitude: %w", err)
}
newGpsInfo.Speed, err = strconv.ParseFloat(strs[7], 64)
if err != nil {
return fmt.Errorf("parse speed: %w", err)
}
// Course sometimes may be null
if len(strs[8]) > 0 {
newGpsInfo.Course, err = strconv.ParseFloat(strs[8], 64)
if err != nil {
return fmt.Errorf("parse course: %w", err)
}
}
*gps = newGpsInfo
return nil
}
func (gps *GpsData) Update(port at.Port) error {
if err := switchGpsMode(port, true); err != nil {
return fmt.Errorf("try to GPS mode: %w", err)
}
defer switchGpsMode(port, false)
resp, err := port.Send("AT+CGPSINFO")
if err != nil {
return fmt.Errorf("receive GPS data: %w", err)
}
if !resp.Check() {
return fmt.Errorf("error response")
}
if err := gps.decode(strings.Split(strings.Replace(resp.RmFront("+CGPSINFO:").String(), "\r", "", -1), "\n")[0]); err != nil {
return fmt.Errorf("decode: %w", err)
}
return nil
}
func switchGpsMode(port at.Port, on bool) error {
onStr := "0"
if on {
onStr = "1"
}
// Reset input
if err := port.GetSerialPort().ResetInputBuffer(); err != nil {
return fmt.Errorf("reset input buffer: %w", err)
}
// Reset output
if err := port.GetSerialPort().ResetOutputBuffer(); err != nil {
return fmt.Errorf("reset output buffer: %w", err)
}
// Check gps mode status
resp, err := port.Send("AT+CGPS?")
if err != nil {
return fmt.Errorf("make at ask: %w", err)
}
if !resp.Check() {
return fmt.Errorf("error response")
}
ans := strings.Replace(strings.Split(strings.Split(resp.RmFront("+CGPS:").String(), "\n")[0], ",")[0], " ", "", -1)
if ans == onStr {
return nil
}
// Modem is not in GPS mode
resp, err = port.Send("AT+CGPS=" + onStr)
if err != nil {
return fmt.Errorf("try to switch to gps: %w", err)
}
if !resp.Check() {
return fmt.Errorf("switch tp GPS failed")
}
return nil
}

83
api/modem/gps/data.go Normal file
View File

@ -0,0 +1,83 @@
package gps
import (
"fmt"
"io"
"log"
"math"
"strconv"
"strings"
"time"
)
type Data struct {
Latitude float64 `json:"Latitude"` // ddmm.mmmmmm
Longitude float64 `json:"Longitude"` // dddmm.mmmmmm
LatitudeIndicator string `json:"Latitude_indicator"` // N/S - North/South
LongitudeIndicator string `json:"Longitude_indicator"` // W/E - West/East
Speed float64 `json:"Speed"`
Course float64 `json:"-"`
Altitude float64 `json:"-"`
Date string `json:"-"`
Time string `json:"-"`
}
var GpsInfoNil = Data{}
func deg2rad(deg float64) float64 {
return deg * (math.Pi / 180)
}
func (gps *Data) CalculateSpeed(newLatitude, newLongitude float64, lastUpdateTime time.Time) {
earthRad := 6371.0 // TODO ?
dLat := deg2rad(math.Abs(newLatitude - gps.Latitude))
dLon := deg2rad(math.Abs(newLongitude - gps.Longitude))
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(deg2rad(newLatitude))*math.Cos(deg2rad(gps.Latitude))*math.Sin(dLon/2)*math.Sin(dLon/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
gps.Speed = earthRad * c / (math.Abs(float64(time.Since(lastUpdateTime))))
}
// To remove warning
// Parse string from AT command that contains gps data
func (gps *Data) decode(str string) error {
var err error
newGpsInfo := Data{}
strs := strings.Split(strings.Split(strings.Replace(str, " ", "", -1), "\n")[0], ",")
logger := log.New(io.Discard, "modem-gps", log.LstdFlags)
if len(strs) < 7 {
return fmt.Errorf("ERROR: too small msg: %s", strs)
}
newGpsInfo.Latitude, err = strconv.ParseFloat(strs[0], 64)
if err != nil {
logger.Println("ERROR parse latitude:", err.Error())
}
newGpsInfo.Longitude, err = strconv.ParseFloat(strs[2], 64)
if err != nil {
logger.Println("ERROR parse longitude:", err.Error())
}
newGpsInfo.LatitudeIndicator = strs[1]
newGpsInfo.LongitudeIndicator = strs[3]
newGpsInfo.Date = strs[4]
newGpsInfo.Time = strs[5]
newGpsInfo.Altitude, err = strconv.ParseFloat(strs[6], 64)
if err != nil {
logger.Println("ERROR parse altitude:", err.Error())
}
newGpsInfo.Speed, err = strconv.ParseFloat(strs[7], 64)
if err != nil {
logger.Println("ERROR parse speed:", err.Error())
}
// Course sometimes may be null
if len(strs[8]) > 0 {
newGpsInfo.Course, err = strconv.ParseFloat(strs[8], 64)
if err != nil {
logger.Println("ERROR parse course:", err.Error())
}
}
*gps = newGpsInfo
return nil
}

107
api/modem/gps/gps.go Normal file
View File

@ -0,0 +1,107 @@
package gps
import (
"fmt"
"io"
"log"
"strings"
"gitea.unprism.ru/KRBL/sim-modem/api/modem/at"
)
type gps struct {
logger *log.Logger
port at.Port
data Data
}
type Gps interface {
Init() error
Update() error
GetData() Data
GetStatus() (Status, error)
io.Closer
}
func New(logger *log.Logger, port at.Port) Gps {
return &gps{
logger: logger,
port: port,
}
}
func (g *gps) Init() error {
if !g.port.IsConnected() {
return fmt.Errorf("at port is not connected")
}
return nil
}
func (g *gps) Update() error {
if err := g.switchGpsMode(true); err != nil {
return fmt.Errorf("try to GPS mode: %w", err)
}
defer g.switchGpsMode(false)
resp, err := g.port.Send("AT+CGPSINFO")
if err != nil {
return fmt.Errorf("receive GPS data: %w", err)
}
if !resp.Check() || !resp.CheckFront("+CGPSINFO:") {
return fmt.Errorf("get GPS info: error response: %s", resp)
}
if err := g.data.decode(strings.Split(strings.Replace(resp.RmFront("+CGPSINFO:").String(), "\r", "", -1), "\n")[0]); err != nil {
g.logger.Printf("error decode: %s\n", err.Error())
}
return nil
}
func (g *gps) GetData() Data {
return g.data
}
func (g *gps) Close() error {
return nil
}
func (g *gps) switchGpsMode(on bool) error {
onStr := "0"
if on {
onStr = "1"
}
// Reset input
if err := g.port.SerialPort().ResetInputBuffer(); err != nil {
return fmt.Errorf("reset input buffer: %w", err)
}
// Reset output
if err := g.port.SerialPort().ResetOutputBuffer(); err != nil {
return fmt.Errorf("reset output buffer: %w", err)
}
// Check gps mode status
resp, err := g.port.Send("AT+CGPS?")
if err != nil {
return err
}
if !resp.Check() || !resp.CheckFront("+CGPS:") {
return fmt.Errorf("get GPS mode: error response: %s", resp)
}
ans := strings.Replace(strings.Split(strings.Split(resp.RmFront("+CGPS:").String(), "\n")[0], ",")[0], " ", "", -1)
if ans == onStr {
return nil
}
// Modem is not in GPS mode
resp, err = g.port.Send("AT+CGPS=" + onStr)
if err != nil {
return err
}
if !resp.Check() {
return fmt.Errorf("set GPS mode: error response: %s", resp)
}
return nil
}

151
api/modem/gps/nmea.go Normal file
View File

@ -0,0 +1,151 @@
package gps
import (
"fmt"
"log"
"strings"
"time"
)
type nmeaFlags int
const (
gga nmeaFlags = 1 << iota // global positioning systemfix data
rmc // recommended minimumspecific GPS/TRANSIT data
gpgsv // GPS satellites in view
gpgsa // GPS DOP and active satellites
vtg // track made good and ground speed
xfi // Global Positioning SystemExtended FixData.)Bit 6 GLGSV (GLONASS satellites in view GLONASSfixesonly
glgsa // 1. GPS/2. Glonass/3. GALILE DOPandActiveSatellites.
gns // fix data for GNSS receivers; output for GPS, GLONASS, GALILEO
_ // Reserved
gagsv // GALILEO satellites in view
_ // Reserved
_ // Reserved
_ // Reserved
_ // Reserved
_ // Reserved
bdpqgsa // BEIDOU/QZSS DOP and activesatellites
bdpqgsv // BEIDOUQZSS satellites in view
nmeaFlagsMask = (1 << 18) - 1
collectTimeout = 1 * time.Second
)
func secondCountDownTimer(title string, logger *log.Logger, t time.Duration) {
_ = title
_ = logger
counter := 0
for {
if counter > int(t.Seconds()) {
break
}
// logger.Printf("%s: %d/%f\n", title, counter, t.Seconds())
time.Sleep(time.Second)
counter += 1
}
}
// Go... otherwise will throw warning
var nmeaFlagsAll = gga | rmc | gpgsv | gpgsa | vtg | xfi | glgsa | gns | gagsv | bdpqgsa | bdpqgsv
func (g *gps) rawCollect(flags nmeaFlags) (string, error) {
// Need to omplement low level write/read here because collect must be atomic operation
// If other command is executed(write/read) it will read a part of nmea report and cause an error
// Setup gps
// Set output rate to 10Hz
if resp, err := g.port.Send("AT+CGPSNMEARATE=1"); err != nil {
return "", err
} else {
if !resp.Check() {
return "", fmt.Errorf("set output rate: error response: %s", resp)
}
}
g.switchGpsMode(true)
g.port.Mutex().Lock()
s := g.port.SerialPort()
defer func() {
g.port.Mutex().Unlock()
g.switchGpsMode(false)
}()
// Send AT+CGPSINFOCFG=255, flags
flags &= nmeaFlagsMask
if _, err := s.Write([]byte("AT+CGPSINFOCFG=1,31\r\n")); err != nil {
return "", fmt.Errorf("serial port write 1: %w", err)
}
// Do I need to read answer
// Wait
secondCountDownTimer("Collecting NMEA data", g.logger, collectTimeout)
// Send AT+CGPSINFOCFG=0, flags
if _, err := s.Write([]byte("AT+CGPSINFOCFG=0,31\r\n")); err != nil {
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
// Read
outBuf := make([]byte, 0)
buf := make([]byte, 128)
readLoop:
for {
n, err := s.Read(buf)
if err != nil {
return string(outBuf), fmt.Errorf("serial port read: %w", err)
}
if n == 0 {
break readLoop
}
outBuf = append(outBuf, buf[:n]...)
}
return string(outBuf), nil
}
func (g *gps) collectNmeaReports(flags nmeaFlags) ([]string, error) {
// Raw collect
resp, err := g.rawCollect(flags)
if err != nil {
return nil, fmt.Errorf("raw collect: %w", err)
}
// DEBUG
g.logger.Println("NMEA raw collect:", resp)
// Right responce struct:
// \r\n
// OK
// \r\n
// (NMEA sentence)...
// \r\n
// OK
// \r\n
strs := strings.Split(strings.Replace(resp, "\r", "", -1), "\n")
// Check
// Now wait for:
// OK
// (NMEA sentence)...
// OK
// if len(strs) < 2 {
// return nil, fmt.Errorf("responce too few rows: %d", len(strs))
// }
// if !(strs[0] == "OK" && strs[len(strs)-1] == "OK") {
// return nil, fmt.Errorf("not OK responce: [% s]", strs)
// }
// This... response is not stable
// Every time it gives one or two OK and in ramdom order
// So I will not check gor it
return strs, nil
}

151
api/modem/gps/status.go Normal file
View File

@ -0,0 +1,151 @@
package gps
import (
"fmt"
"strconv"
"strings"
)
type Status struct {
GotResponses bool `json:"gotResponses"`
IsValidData bool `json:"isValidData"`
FoundSatelitesCount int `json:"foundSatelitesCount"`
ActiveSatelitesCount int `json:"activeSatelitesCount"`
Rms float32 `json:"rms"` // Root mean square
}
var StatusNil = Status{
GotResponses: false,
IsValidData: false,
FoundSatelitesCount: 0,
ActiveSatelitesCount: 0,
Rms: 0,
}
func (g *gps) GetStatus() (Status, error) {
// Provides more information about signal and possible problems using NMEA reports
// Collect reports
reports, err := g.collectNmeaReports(nmeaFlagsAll) // Now minimum
if err != nil {
return StatusNil, fmt.Errorf("collect nmea reports: %w", err)
}
// Annalise
st := Status{}
checkLoop:
for _, s := range reports {
// Check for NMEA format
if len(s) < 1 || s[0] != '$' {
continue checkLoop
}
st.GotResponses = true
// g.logger.Println("NMEA check:", s)
values := strings.Split(s, ",")
if len(values[0]) != 6 {
return StatusNil, fmt.Errorf("nmea invalid sentence: %s", s)
}
switch values[0][3:] { // Switch by content
case "GSV": // Any satelites
// g.logger.Println("check GSV")
// Check len
if len(values) < 17 {
g.logger.Println("GSV too small values")
continue checkLoop
}
// Decode
// 0 - msg type
// 1 - number of msgs
// 2 - index of this msg
// 3 - number of visible satelites
// 4: - other data
// Msg index
index, err := strconv.Atoi(values[3])
if err != nil {
g.logger.Println("GSV too small values")
continue checkLoop
}
_ = index
// if index != 0 {
// g.logger.Println("discard not first GSV msg")
// continue checkLoop
// }
// Count
satCount, err := strconv.Atoi(values[4])
if err != nil {
g.logger.Println("GSV too small values")
continue checkLoop
}
st.FoundSatelitesCount = satCount
case "GSA": // Active satelites
// g.logger.Println("check GSA")
// Check len
if len(values) < 17 {
g.logger.Println("GSV too small values")
continue checkLoop
}
// Decode
// 0 - msg type
// 1 - mode of selecting format
// 2 - mode of selected format
// 3:15 - IDs of active satelites
// 15: - other data
// Counting active satelites
count := 0
for _, v := range values[3:15] {
if _, err := strconv.Atoi(v); err == nil {
count += 1
}
}
st.ActiveSatelitesCount = max(st.ActiveSatelitesCount, count)
case "RMC": // Minimum GPS data
// g.logger.Println("check RMC")
// Check len
if len(values) < 12 {
g.logger.Println("RMC too small values")
continue checkLoop
}
// Decode
// 0 - msg type
// 1 - time
// 2 - is data valid or not
// 3: - other data
// Is valid value
if values[2] == "A" {
st.IsValidData = true
}
case "GST":
// g.logger.Println("check GST")
// Check len
if len(values) < 8 {
g.logger.Println("GST too small values")
continue checkLoop
}
// Decode
// 0 - msg type
// 1 - time
// 2 - Root Mean Square
// 3: - other data
rms, err := strconv.ParseFloat(values[2], 32)
if err != nil {
g.logger.Println("RMS decode:", err.Error())
continue checkLoop
}
st.Rms = float32(rms)
}
}
return st, nil
}

View File

@ -1,51 +1,53 @@
package internet package internet
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"log" "log"
"os"
"os/exec" "os/exec"
"strconv"
"strings" "strings"
"time"
"github.com/CGSG-2021-AE4/modem-test/api/modem/at" "gitea.unprism.ru/KRBL/sim-modem/api/modem/at"
) )
var apns = map[string]string{ const (
"Tinkoff": "m.tinkoff", pingPacketsCount = 3
} pingTimeout = 5
inetConnectedTimeout = 4 * time.Second
const pppConfigName = "annalistnet" pingAddr = "8.8.8.8"
const pppConfig = ` ifName = "ppp0" // Interface name
# Annalist project custom internet connection inetMetric = 2000
)
# APN:
connect "/usr/sbin/chat -v -f /etc/chatscripts/gprs -T %s"
# Port:
%s
# Baudrate:
%d
noipdefault
usepeerdns
defaultroute
persist
noauth
nocrtscts
local
`
type conn struct { type conn struct {
logger *log.Logger logger *log.Logger
port at.Port port at.Port
pppPort string
isConnectExecuted bool
isInited bool
isRouteSet bool
connectTime time.Time
gw string // Gateway
} }
type Conn interface { type Conn interface {
Init() error Init(pppPort string) error
ConfigurePPP() error
Ping() bool // Is connected Connect() error
Disconnect() error
SetDefaultRouteTable() error
UnsetDefaultRouteTable() error
IsConnected() bool // Check interface existance
Ping() error
io.Closer io.Closer
} }
@ -53,190 +55,256 @@ func New(logger *log.Logger, port at.Port) Conn {
return &conn{ return &conn{
logger: logger, logger: logger,
port: port, port: port,
isConnectExecuted: false,
isInited: false,
isRouteSet: false,
} }
} }
func (c *conn) checkPackageExist(pname string) bool { func (c *conn) Init(pppPort string) error {
resp, err := exec.Command("apt-mark", "showmanual", pname).Output() c.pppPort = pppPort
c.logger.Println("CHECK:", resp) // Setup only setup
if err != nil {
c.logger.Println("CHECK PACKAGE ERROR: ", err.Error())
return false
}
return string(resp[:len(pname)]) == pname
}
func (c *conn) ensurePackage(pname string) error {
if c.checkPackageExist(pname) {
return nil
}
return fmt.Errorf("package %s not installed", pname)
// c.logger.Println("Installing", pname, "package...")
// resp, err := exec.Command("apt-get", "install", pname).Output()
// if err != nil {
// return fmt.Errorf("execute install cmd: %w", err)
// }
// c.logger.Println(resp)
// c.logger.Println("\x1b[38;2;255;0;0mComplete\x1b[38;2;255;255;255m")
// return nil
}
// Check requirenments
func (c *conn) checkReqs() error {
// Check AT port for sure
if c.port == nil || !c.port.IsConnected() {
return fmt.Errorf("AT port is not connect or nil")
}
// Ensure all necessary packages installed
if err := c.ensurePackage("ppp"); err != nil {
return fmt.Errorf("ensure ppp package: %w", err)
}
// if err := c.ensurePackage("net-tools"); err != nil {
// return fmt.Errorf("ensure net-tools package: %w", err)
// }
// Check SIM is valid
// AT+CPIN? and just check
resp, err := c.port.Send("AT+CPIN?")
if err != nil {
return fmt.Errorf("AT+CPIN? request: %w", err)
}
if !resp.Check() {
return fmt.Errorf("SIM card is not inserted")
}
return nil
}
func (c *conn) ConfigurePPP() error {
// Get provider name and its APN
resp, err := c.port.Send("AT+CSPN?")
if err != nil {
return fmt.Errorf("AT+CSPN? request: %w", err)
}
if !resp.Check() {
return fmt.Errorf("failed to check SIM provider")
}
strs := strings.Split(string(resp), "\"")
if len(strs) < 3 {
return fmt.Errorf("parse AT+CSPN response: %s", string(resp))
}
provider := strs[1]
apn := apns[provider]
if apn == "" {
return fmt.Errorf("no apn for provider: %s", provider)
}
// Make config
c.logger.Printf("Config values: %s, %s, %d", apn, c.port.GetName(), c.port.GetBaudrate())
config := fmt.Sprintf(pppConfig, apn, c.port.GetName(), c.port.GetBaudrate())
// Write to file
f, err := os.OpenFile("/etc/ppp/peers/"+pppConfigName, os.O_CREATE | os.O_WRONLY, 0666)
if err != nil {
return fmt.Errorf("open ppp config file %w", err)
}
defer f.Close()
if _, err := f.Write([]byte(config)); err != nil {
return fmt.Errorf("write to ppp config file: %w", err)
}
return nil
}
func (c *conn) setup() error {
c.logger.Println("Check requirenments...")
if err := c.checkReqs(); err != nil {
return fmt.Errorf("check requirenments: %w", err)
}
// DEBUG show networks and signal
resp, err := c.port.Send("AT+COPS?")
if err != nil {
return fmt.Errorf("AT+COPS? request: %w", err)
}
c.logger.Println("DEBUG networks:", resp)
resp, err = c.port.Send("AT+CSQ")
if err != nil {
return fmt.Errorf("AT+CSQ request: %w", err)
}
c.logger.Println("DEBUG signal quality:", resp)
// Configure ppp
// what is better ASK If /etc/ppp/peers/annalistnet not exists
c.logger.Println("Configure ppp...")
if err := c.ConfigurePPP(); err != nil {
return fmt.Errorf("configure ppp: %w", err)
}
return nil
}
func (c *conn) connect() error {
resp, err := exec.Command("pon", pppConfigName).Output()
if err != nil {
return fmt.Errorf("execute connect cmd: %w", err)
}
c.logger.Println("DEBUG pon response:", string(resp))
return nil
}
func (c *conn) diconnect() error {
resp, err := exec.Command("poff", pppConfigName).Output()
if err != nil {
return fmt.Errorf("execute disconnect cmd: %w", err)
}
c.logger.Println("DEBUG poff response:", string(resp))
return nil
}
func (c *conn) Init() error {
// Setup
c.logger.Println("Setup...")
if err := c.setup(); err != nil { if err := c.setup(); err != nil {
return fmt.Errorf("setup: %w", err) return fmt.Errorf("setup: %w", err)
} }
// Connect c.isInited = true
c.logger.Println("Connect...") return nil
if err := c.connect(); err != nil { }
return fmt.Errorf("connect: %w", err)
}
//DEBUG func (c *conn) Connect() error {
resp, err := exec.Command("ifconfig").Output() if !c.isInited {
return fmt.Errorf("internet submodule is not inited")
}
// Check is already connected
if c.isConnectExecuted {
return fmt.Errorf("already connected")
}
// Check signal
resp, err := exec.Command("pon", pppConfigName).Output()
if err != nil { if err != nil {
return fmt.Errorf("execute ifconfig cmd: %w", err) return fmt.Errorf("execute pon cmd: %w", err)
}
if len(resp) > 0 {
c.logger.Println("pon response:", string(resp))
} }
c.logger.Println("DEBUG ifconfig resp:", string(resp))
// Test connectin using Ping c.isConnectExecuted = true
c.logger.Println("Test...") c.connectTime = time.Now()
if !c.Ping() { c.gw, err = c.GetHostIp()
return fmt.Errorf("ping failed") if err != nil {
return fmt.Errorf("get host ip: %w", err)
}
c.logger.Println("\x1b[38;2;0;255;0mInternet connected.\x1b[38;2;255;255;255m")
return nil
}
func (c *conn) Disconnect() error {
// return nil // Temporary do not turn off inet
if !c.isConnectExecuted {
return nil
// return fmt.Errorf("internet is not connected")
}
c.isConnectExecuted = false
resp, err := exec.Command("poff", pppConfigName).Output()
if err != nil {
return fmt.Errorf("execute poff cmd: %w", err)
}
if len(resp) > 0 {
c.logger.Println("poff response:", string(resp))
}
c.isConnectExecuted = false
c.logger.Println("\x1b[38;2;0;255;0mInternet disconnected.\x1b[38;2;255;255;255m")
return nil
}
func (c *conn) SetDefaultRouteTable() error {
// route add -net default gw 10.64.64.64 metric 2000 dev ppp0
resp, err := exec.Command("route", "add", "-net", "default", "gw", c.gw, "metric", strconv.Itoa(inetMetric), "dev", ifName).Output()
if err != nil {
return fmt.Errorf("execute add cmd: %w", err)
}
// Check response
if len(resp) != 0 {
c.logger.Println("Not nil response:", string(resp))
}
c.isRouteSet = true
c.logger.Println("\x1b[38;2;0;255;0mInternet route table set.\x1b[38;2;255;255;255m")
return nil
}
func (c *conn) UnsetDefaultRouteTable() error {
if !c.isRouteSet {
return fmt.Errorf("route table is not set")
}
resp, err := exec.Command("route", "del", "-net", "default", "gw", c.gw, "metric", strconv.Itoa(inetMetric), "dev", ifName).Output()
if err != nil {
return fmt.Errorf("execute del cmd: %w", err)
}
// Check response
if len(resp) != 0 {
c.logger.Println("Not nil response:", string(resp))
}
c.logger.Println("\x1b[38;2;0;255;0mInternet route table unset.\x1b[38;2;255;255;255m")
return nil
}
func (c *conn) ping(flags []string, timeout int) error {
c.logger.Println("Ping", flags[len(flags)-1])
// Just counter
ctx, cancel := context.WithCancel(context.Background())
go func(c *conn, ctx context.Context) {
for i := 0; i < timeout; i++ {
c.logger.Printf("Ping %d/%d", i, timeout)
select {
case <-ctx.Done():
return
case <-time.After(time.Second):
}
}
}(c, ctx)
// Executing cmd
cmd := exec.Command("ping", flags...)
resp, err := cmd.Output()
cancel()
if err != nil {
c.logger.Println("Ping default interface cmd error:", err)
}
// Parse
lines := strings.Split(string(resp), "\n")
// Look for string "--- *.*.*.* ping statistics ---" by first simbol '-'
stLineI := 0
searchStLineLoop:
for i, l := range lines {
if len(l) > 0 && l[0] == '-' {
stLineI = i + 1
break searchStLineLoop
}
}
if stLineI == 0 || stLineI >= len(lines) {
return fmt.Errorf("failed to find statistics line: %d", stLineI)
}
stStr := lines[stLineI]
// Get third value "packet lost"
values := strings.Split(stStr, ",")
if len(values) < 3 {
return fmt.Errorf("invalid statistic values(len): [%s]", values)
}
// Get number
words := strings.Split(values[2], " ")
if len(words) < 2 {
return fmt.Errorf("invalid \"packets lost\" value(words count): [%s]", words)
}
// First is ''
// Second is '...%'
packetsLost, err := strconv.Atoi(words[1][:len(words[1])-1]) // Without '%' char
if err != nil {
return fmt.Errorf("parse \"packets lost\" value: %w", err)
}
if packetsLost == 100 {
return fmt.Errorf("lost all packages")
}
if packetsLost > 0 {
c.logger.Printf("lost some packets: %d%%\n", packetsLost)
} }
return nil return nil
} }
func (c *conn) Ping() bool { func (c *conn) GetHostIp() (string, error) {
// Test - try to connect to Google DNS if !c.isConnectExecuted {
// ping -I ppp0 8.8.8.8 return "", fmt.Errorf("internet not connected")
resp, err := exec.Command("ping", "8.8.8.8").Output()
if err != nil {
c.logger.Println("Ping 1 cmd error:", err)
} }
c.logger.Println("Ping 1 resp:", string(resp)) // Wait some time for system to setup route table
time.Sleep(time.Until(c.connectTime.Add(inetConnectedTimeout)))
resp, err = exec.Command("ping", "-I", "ppp0", "8.8.8.8").Output() // Execute cmd
resp, err := exec.Command("route").Output()
if err != nil { if err != nil {
c.logger.Println("Ping 2 cmd error:", err) return "", fmt.Errorf("exec route cmd: %w", err)
} }
c.logger.Println("Ping 2 resp:", string(resp)) // Check and split to lines
lines := strings.Split(string(resp), "\n")
if len(lines) <= 3 || lines[0] != "Kernel IP routing table" {
return "", fmt.Errorf("invalid route response: [% s]", lines)
}
// Search line about ppp interface
searchLoop:
for _, l := range lines[1:] {
words := strings.Fields(l)
if len(words) != 8 {
/// c.logger.Printf("invalid route line(words number): [%s]\n", words)
continue searchLoop
}
if words[7] == ifName {
if words[3] != "UH" {
// c.logger.Println("invalid flags")
continue searchLoop
}
return words[0], nil
}
}
return "", fmt.Errorf("found no suitable ppp interface")
}
func (c *conn) PingDefault() error {
return c.ping([]string{"-c", strconv.Itoa(pingPacketsCount), "-w", strconv.Itoa(pingTimeout), pingAddr}, pingTimeout)
}
return !strings.Contains(string(resp), "Destination Host Unreachable") // tmp solution func (c *conn) PingPPP() error {
return c.ping([]string{"-I", ifName, "-c", string(pingPacketsCount), "-w", string(pingTimeout), pingAddr}, pingTimeout)
}
func (c *conn) Ping() error {
return c.PingDefault()
}
func (c *conn) IsConnected() bool {
if !c.isConnectExecuted {
return false
}
// Make "ifconfig" request
resp, err := exec.Command("ifconfig").Output()
if err != nil {
c.logger.Println("ifconfig cmd error:", err.Error())
return false
}
lines := strings.Split(string(resp), "\n")
for _, l := range lines {
if len(l) == 0 {
continue
}
if l[0] == ' ' {
continue
}
interfaceName := strings.Split(l, ":")[0]
if interfaceName == ifName {
return true
}
}
return false // Did not found
} }
func (c *conn) Close() error { func (c *conn) Close() error {
if err := c.diconnect(); err != nil { c.isInited = false
return fmt.Errorf("diconnect: %w", err) // Unset route table
if c.isRouteSet {
if err := c.UnsetDefaultRouteTable(); err != nil {
c.logger.Println("unset route table error:", err.Error())
}
}
// Disconnect
if c.isConnectExecuted {
if err := c.Disconnect(); err != nil {
c.logger.Println("diconnect error:", err.Error())
}
} }
return nil return nil
} }

131
api/modem/internet/setup.go Normal file
View File

@ -0,0 +1,131 @@
package internet
import (
"fmt"
"os"
"os/exec"
"strings"
"gitea.unprism.ru/KRBL/sim-modem/api/modem/utils"
)
var apns = map[string]string{
"Tinkoff": "m.tinkoff",
"Megafon": "internet",
"BeeLine": "internet.beeline.ru",
}
const pppConfigName = "annalistnet"
const pppConfig = `
# Annalist project custom internet connection
# APN:
connect "/usr/sbin/chat -v -f /etc/chatscripts/gprs -T %s"
# Port:
%s
# Baudrate:
%d
noipdefault
usepeerdns
defaultroute
persist
noauth
nocrtscts
local
`
func (c *conn) setup() error {
if err := c.checkReqs(); err != nil {
return fmt.Errorf("check requirenments: %w", err)
}
// Configure ppp
// what is better ASK If /etc/ppp/peers/annalistnet not exists
if err := c.configurePPP(); err != nil {
return fmt.Errorf("configure ppp: %w", err)
}
return nil
}
// Check requirenments
func (c *conn) checkReqs() error {
// Check AT port for sure
if c.port == nil || !c.port.IsConnected() {
return fmt.Errorf("AT port is not connect or nil")
}
// Ensure all necessary packages installed
if err := c.ensurePackage("ppp"); err != nil {
return fmt.Errorf("ensure ppp package: %w", err)
}
// if err := c.ensurePackage("net-tools"); err != nil {
// return fmt.Errorf("ensure net-tools package: %w", err)
// }
// Check SIM is valid
if err := utils.CheckPIN(c.port, c.logger); err != nil {
return fmt.Errorf("PIN check: %w", err)
}
return nil
}
func (c *conn) ensurePackage(pname string) error {
if c.checkPackageExist(pname) {
return nil
}
return fmt.Errorf("package %s not installed", pname)
// c.logger.Println("Installing", pname, "package...")
// resp, err := exec.Command("apt-get", "install", pname).Output()
// if err != nil {
// return fmt.Errorf("execute install cmd: %w", err)
// }
// c.logger.Println(resp)
// c.logger.Println("\x1b[38;2;255;0;0mComplete\x1b[38;2;255;255;255m")
// return nil
}
func (c *conn) checkPackageExist(pname string) bool {
resp, err := exec.Command("apt-mark", "showmanual", pname).Output()
if err != nil {
c.logger.Println("check package error: ", err.Error())
return false
}
return string(resp[:len(pname)]) == pname
}
func (c *conn) configurePPP() error {
// Get provider name and its APN
resp, err := c.port.Send("AT+CSPN?")
if err != nil {
return fmt.Errorf("AT+CSPN? request: %w", err)
}
if !resp.Check() {
return fmt.Errorf("get provider: error response: %s", resp)
}
strs := strings.Split(string(resp), "\"")
if len(strs) < 3 {
return fmt.Errorf("parse AT+CSPN response: %s", string(resp))
}
provider := strs[1]
apn := apns[provider]
if apn == "" {
return fmt.Errorf("no apn for provider: %s", provider)
}
// Make config
c.logger.Printf("Config ppp values: %s, %s, %d", apn, c.pppPort, c.port.GetBaudrate())
config := fmt.Sprintf(pppConfig, apn, c.pppPort, c.port.GetBaudrate())
// Write to file
f, err := os.OpenFile("/etc/ppp/peers/"+pppConfigName, os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
return fmt.Errorf("open ppp config file %w", err)
}
defer f.Close()
if _, err := f.Write([]byte(config)); err != nil {
return fmt.Errorf("write to ppp config file: %w", err)
}
return nil
}

View File

@ -1,202 +1,300 @@
package modem package modem
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/CGSG-2021-AE4/modem-test/api/modem/at" "gitea.unprism.ru/KRBL/sim-modem/api/modem/at"
"github.com/CGSG-2021-AE4/modem-test/api/modem/gpio" "gitea.unprism.ru/KRBL/sim-modem/api/modem/gpio"
"github.com/CGSG-2021-AE4/modem-test/api/modem/internet" "gitea.unprism.ru/KRBL/sim-modem/api/modem/gps"
"github.com/CGSG-2021-AE4/modem-test/api/modem/sms" "gitea.unprism.ru/KRBL/sim-modem/api/modem/internet"
"gitea.unprism.ru/KRBL/sim-modem/api/modem/sms"
) )
// yy/MM/dd,hh:mm:ss+zzzz
const timeLayout = "06/01/02,15:04:05-0700"
var ttyPorts = []string{"ttyUSB1", "ttyUSB2", "ttyUSB3", "ttyS0", "ttyAMA2"}
var availableModels = []string{"SIMCOM_SIM7600E-H", "SIMCOM_SIM808"}
type ModemData struct { type ModemData struct {
Port string `json:"Port"` Port string `json:"Port"`
GpsData gps.Data
} }
type modem struct { type modem struct {
// Internal values // Internal values
logger *log.Logger logger *log.Logger
mutex sync.Mutex
// Serial values // Serial values
baudrate int baudrate int
deviceName string deviceName string
port at.Port port at.Port
model string
// 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
// GPS // GPS
gpsInfo GpsData gps gps.Gps
// Internet connection // Internet connection
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
GetInfo() ModemData GetData() ModemData
GetTime() (time.Time, error)
// Temp access to SMS and AT interface PowerOn() error
Sms() sms.Dialer PowerOnCtx(ctx context.Context) error // Because it takes ~30 seconds
At() at.Port PowerOff() error
// Access to SMS, GPS, AT interfaces mostly for debug
At() at.Port // Send
Gps() gps.Gps // Update, GetData, GetStatus
Sms() sms.Sms // Send, ReadNew
Ic() internet.Conn // Connect, Disconnect
io.Closer io.Closer
} }
func New(logger *log.Logger) Modem { func New(logger *log.Logger) Modem {
return &modem{ return &modem{
logger: logger, logger: logger,
baudrate: 115200, baudrate: 115200,
onOffPin: gpio.New(log.New(logger.Writer(), "gpio", log.LstdFlags), 6), onOffPin: gpio.New(log.New(logger.Writer(), "gpio", log.LstdFlags), 6),
lastUpdateTime: time.Now(), lastUpdateTime: time.Now(),
} }
} }
func (m *modem) Init() error { func (m *modem) Init() error {
m.mutex.Lock()
defer m.mutex.Unlock()
// Turn module on // Turn module on
m.logger.Println("=============================== Turn on module") m.logger.Println("=============================== Init gpio")
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()
// Search // Search
m.logger.Println("=============================== Search") m.logger.Println("=============================== Search")
// Soft search ports, err := m.searchPort(true)
if err := m.searchPort(true); err != nil { if err != nil {
return fmt.Errorf("soft port search: %w", err) return fmt.Errorf("soft port search: %w", err)
} }
// Wide search if len(ports) == 0 {
if m.port == nil { // Wide search
if err := m.searchPort(false); err != nil { ports, err := m.searchPort(true)
return fmt.Errorf("not soft port search: %w", err) if err != nil {
return fmt.Errorf("wide port search: %w", err)
}
if len(ports) == 0 {
return fmt.Errorf("no AT ports found")
} }
} }
if m.port == nil { if len(ports) == 1 {
return fmt.Errorf("no port is detected") // return fmt.Errorf("only one AT port found")
m.logger.Println("!!!!! only one AT port found")
} }
// !!!!
// Internet connection and serial connection on one port is impossible, so:
// port[0] is for serial
// port[1] is for internet(ppp)
m.logger.Println(ports)
// Connect // Connect
m.logger.Println("=============================== Connect") m.logger.Println("=============================== Connect")
m.port = at.New(m.logger, ports[0], m.baudrate)
if err := m.connect(); err != nil { if err := m.connect(); err != nil {
return fmt.Errorf("connect: %w", err) return fmt.Errorf("connect: %w", err)
} }
// Tests // Init submodules
m.logger.Println("=============================== Test") submodulesLogger := m.logger.Writer() // FOR more logs
if err := m.testGPS(); err != nil { // submodulesLogger := io.Discard // FOR less logs
return fmt.Errorf("testGPS: %w", err)
m.logger.Println("=============================== Init submodules")
m.ic = internet.New(log.New(submodulesLogger, "modem-internet : ", log.LstdFlags), m.port)
if err := m.ic.Init(ports[len(ports)-1]); 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")
} }
// // Establish internet connection // m.sms = sms.New(log.New(submodulesLogger, "modem-sms : ", log.LstdFlags), m.port)
// m.logger.Println("=============================== Internet") // if err := m.sms.Init(); err != nil {
// m.ic = internet.New(log.New(m.logger.Writer(), "internet", log.LstdFlags), m.port) // m.logger.Printf("\x1b[38;2;255;0;0mSMS: %s\x1b[38;2;255;255;255m\n", err.Error())
// if err := m.ic.Init(); err != nil { // } else {
// return fmt.Errorf("internet connection init: %w", err) // m.logger.Println("\x1b[38;2;0;255;0mSMS OK\x1b[38;2;255;255;255m")
// } // }
// Init sms dialer m.gps = gps.New(log.New(submodulesLogger, "modem-gps : ", log.LstdFlags), m.port)
m.sms = sms.New(log.New(m.logger.Writer(), "sms", log.LstdFlags), m.port) if err := m.gps.Init(); err != nil {
if err := m.sms.Init(); err != nil { m.logger.Printf("\x1b[38;2;255;0;0mgps init %w\x1b[38;2;255;255;255m\n", err)
return fmt.Errorf("sms dialer init %w", err) } else {
m.logger.Println("\x1b[38;2;0;255;0mGPS OK\x1b[38;2;255;255;255m")
} }
return nil
}
func (m *modem) Validate() bool { // Tests
return m.isConnected() // GPS works fine but almost always there is no signal
// m.logger.Println("=============================== Test")
// if err := m.testGPS(); err != nil {
// return fmt.Errorf("testGPS: %w", err)
// }
return nil
} }
func (m *modem) Update() error { func (m *modem) Update() error {
if !m.isConnected() { m.mutex.Lock()
defer m.mutex.Unlock()
if !m.IsConnected() {
m.logger.Println("No connection to module") m.logger.Println("No connection to module")
return nil return nil
} }
m.logger.Println("Update") m.logger.Println("Update", m.gps)
// ans, err := m.port.Request(at.CmdQuestion, "CGPSINFO") if err := m.gps.Update(); err != nil {
// if err != nil { m.logger.Println("gps update:", err.Error())
// return fmt.Errorf("check GPS info mode: %w", err) }
// } // Read new messages
// ok := ans == "1"
// if !ok {
// _, err := m.port.Request(at.CmdCheck, "CGPSINFO")
// if err != nil {
// return fmt.Errorf("switch to GPS info mode: %w", err)
// }
// m.logger.Println("switched to GPS mode")
// } else {
// m.logger.Println("mode in right GPS mode")
// }
// Update
m.logger.Println("Receiving GPS data...")
resp, err := m.port.Send("AT+CGPSINFO")
if err != nil {
return fmt.Errorf("receive GPS data: %w", err)
}
if !resp.Check() {
return fmt.Errorf("error response")
}
m.logger.Println("Decoding data...")
if err := m.gpsInfo.decode(strings.Split(strings.Replace(resp.RmFront("+CGPSINFO:").String(), "\r", "", -1), "\n")[0]); err != nil {
m.logger.Println("Gps info decode error:", err.Error())
return nil
}
m.logger.Println("Decoded successfully")
return nil return nil
} }
func (m *modem) GetInfo() ModemData { func (m *modem) GetData() ModemData {
m.mutex.Lock()
defer m.mutex.Unlock()
return ModemData{ return ModemData{
Port: m.port.GetName(), Port: m.port.GetName(),
GpsData: m.gpsInfo, Data: m.gps.GetData(),
} }
} }
func (m *modem) Sms() sms.Dialer { func (m *modem) GetTime() (time.Time, error) {
// Make request
resp, err := m.port.Send("AT+CCLK?")
if err != nil {
return time.Time{}, err
}
if !resp.Check() || !resp.CheckFront("+CCLK: ") {
return time.Time{}, fmt.Errorf("CCLK? error response: %s", resp.String())
}
// Extract time string
values := strings.Split(strings.Split(resp.String(), "\n")[0], "\"")
if len(values) < 2 {
return time.Time{}, fmt.Errorf("invalid values (len): [%s]", values)
}
timeStr := values[1]
m.logger.Println("Raw time:", timeStr)
if len(timeStr) != len("yy/MM/dd,hh:mm:ss+zz") {
return time.Time{}, fmt.Errorf("invalid time string: %s", timeStr)
}
// Convert time zone
timeZoneStr := timeStr[len(timeStr)-2:]
timeZone, err := strconv.Atoi(timeZoneStr)
if err != nil {
return time.Time{}, fmt.Errorf("parse time zone: %w", err)
}
timeStr = fmt.Sprintf("%s%02d%02d", timeStr[:len(timeStr)-2], timeZone/4, timeZone%4)
// Parse to golang time
return time.Parse(timeLayout, timeStr)
}
func (m *modem) PowerOn() error {
m.onOffPin.PowerOn() // DEBUG do not want to wait 30 seconds
return nil
}
func (m *modem) PowerOnCtx(ctx context.Context) error {
m.onOffPin.PowerOnCtx(ctx) // DEBUG do not want to wait 30 seconds
return nil
}
func (m *modem) PowerOff() error {
_, err := m.At().Send("AT+CPOF")
return err
}
func (m *modem) restart() error {
m.PowerOff()
time.Sleep(10 * time.Second)
m.PowerOn()
return nil
}
func (m *modem) Sms() sms.Sms {
return m.sms return m.sms
} }
func (m *modem) Gps() gps.Gps {
return m.gps
}
func (m *modem) At() at.Port { func (m *modem) At() at.Port {
return m.port return m.port
} }
func (m *modem) Close() error { func (m *modem) Ic() internet.Conn {
if err := m.sms.Close(); err != nil { return m.ic
return fmt.Errorf("sms: %w", err) }
func (m *modem) Close() error { // I can't return error so I log it
m.mutex.Lock()
defer m.mutex.Unlock()
// Close submodules
if m.sms != nil {
if err := m.sms.Close(); err != nil {
m.logger.Printf("\x1b[38;2;255;0;0mclose sms error: %s\x1b[38;2;255;255;255m\n", err.Error())
}
}
if m.ic != nil {
if err := m.ic.Close(); err != nil {
m.logger.Printf("\x1b[38;2;255;0;0mclose internet error: %s\x1b[38;2;255;255;255m\n", err.Error())
}
}
if m.gps != nil {
if err := m.gps.Close(); err != nil {
m.logger.Printf("\x1b[38;2;255;0;0mclose gps error: %s\x1b[38;2;255;255;255m\n", err.Error())
}
} }
// Not right way I think // Close gpio and serial
if err := m.port.Close(); err != nil {
return fmt.Errorf("serial port: %w", err)
}
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.port.Close(); err != nil {
return fmt.Errorf("internet connection: %w", err) m.logger.Printf("\x1b[38;2;255;0;0mclose serial port 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")
@ -211,7 +309,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()
} }
@ -231,7 +329,8 @@ func (m *modem) testGPS() error {
// Difference: I do not set \n at the end of string // Difference: I do not set \n at the end of string
func (m *modem) getShortInfo() string { func (m *modem) getShortInfo() string {
return fmt.Sprintf("%f,%s,%f,%s", m.gpsInfo.Latitude, m.gpsInfo.LatitudeIndicator, m.gpsInfo.Longitude, m.gpsInfo.LongitudeIndicator) d := m.gps.GetData()
return fmt.Sprintf("%f,%s,%f,%s", d.Latitude, d.LatitudeIndicator, d.Longitude, d.LongitudeIndicator)
} }
func (m *modem) saveGPS(path string) error { func (m *modem) saveGPS(path string) error {
@ -247,22 +346,67 @@ 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 {
// Reset input
if err := m.port.SerialPort().ResetInputBuffer(); err != nil {
return fmt.Errorf("reset input buffer: %w", err)
}
// Reset output
if err := m.port.SerialPort().ResetOutputBuffer(); err != nil {
return fmt.Errorf("reset output buffer: %w", err)
}
// m.restart()
// These commands ensure that correct modes are set
// m.port.RawSend("\r\n\x1A\r\n") // Sometimes enables echo mode
//if m.At().GetName() == "/dev/ttyUSB0" {
// buf := make([]byte, 256)
// for i := 0; i < 10; i++ {
// len, err := m.port.SerialPort().Read(buf)
// if err != nil {
// m.logger.Println("ERROR:", err.Error())
// }
// if len != 0 {
// m.logger.Println(string(buf[:len]))
// }
// time.Sleep(time.Second)
// m.logger.Println(".")
// }
//}
m.printCmd("AT") // Sometimes enables echo mode
m.printCmd("AT") // Sometimes enables echo mode
m.printCmd("ATE0") // Sometimes enables echo mode
m.printCmd("AT+CGPSFTM=0") // Sometimes does not turn off nmea
m.printCmd("AT+CMEE=2") // Turn on errors describtion
m.printCmd("AT+CTZU=1") // Turn on time update
return nil
}
func (m *modem) checkCurPortDead() error {
if err := m.port.RawSend("AT\r\n"); err != nil {
return fmt.Errorf("raw send: %w", err)
}
if resp, err := m.port.RawRead(time.Second); err != nil || len(resp) == 0 {
if err != nil {
return fmt.Errorf("raw read: %w", err)
}
return fmt.Errorf("read 0")
}
return nil
}
func (m *modem) checkPort(portName string) (outErr error) { func (m *modem) checkPort(portName string) (outErr error) {
defer func() { defer func() {
if outErr != nil { // Clear port if there is error if outErr != nil { // Clear port if there is error
@ -283,16 +427,17 @@ func (m *modem) checkPort(portName string) (outErr error) {
} }
defer m.port.Disconnect() // Do not bother about errors... defer m.port.Disconnect() // Do not bother about errors...
// Reset input // m.restart()
if err := m.port.GetSerialPort().ResetInputBuffer(); err != nil {
return fmt.Errorf("reset input buffer: %w", err) // To filter dead ports
if err := m.checkCurPortDead(); err != nil {
return fmt.Errorf("echo: %w", err)
} }
// Reset output
if err := m.port.GetSerialPort().ResetOutputBuffer(); err != nil { if err := m.setupPort(); err != nil {
return fmt.Errorf("reset output buffer: %w", err) return fmt.Errorf("setup port: %w", err)
} }
m.port.Send("ATE0") // This shit sometimes enables echo mode... why... when... but it can
// m.port.Send("\r\n")
// Ping // Ping
m.logger.Println("Ping...") m.logger.Println("Ping...")
@ -308,64 +453,73 @@ func (m *modem) checkPort(portName string) (outErr error) {
return fmt.Errorf("get model: %w", err) return fmt.Errorf("get model: %w", err)
} }
if !resp.Check() { if !resp.Check() {
return fmt.Errorf("error response: %s", resp) return fmt.Errorf("get model: error response: %s", resp)
} }
model := strings.Split(resp.String(), "\n")[0] model := strings.Split(resp.String(), "\n")[0]
if err != nil { if err != nil {
return fmt.Errorf("get model: %w", err) return fmt.Errorf("get model: %w", err)
} }
rightModel := "SIMCOM_SIM7600E-H"
// m.logger.Printf("[% x]\n [% x]", []byte("SIMCOM_SIM7600E-H"), []byte(model)) // Check model
if len(model) >= len(rightModel) && model[:len(rightModel)] != rightModel { foundModel := ""
return fmt.Errorf("invalid modem model: %s", model) for _, rightModel := range availableModels {
if len(model) >= len(rightModel) && model[:len(rightModel)] == rightModel {
foundModel = rightModel
break
}
} }
if foundModel == "" {
return fmt.Errorf("invalid model: %s", model)
}
m.model = foundModel
m.logger.Println("\x1b[38;2;0;255;0mOK\x1b[38;2;255;255;255m") m.logger.Println("\x1b[38;2;0;255;0mOK\x1b[38;2;255;255;255m")
return nil return nil
} }
func (m *modem) searchPort(isSoft bool) error {
// Get ports
ports := []string{"ttyUSB1", "ttyUSB2", "ttyUSB3"}
if !isSoft {
ps, err := getTtyDevices()
if err != nil {
fmt.Errorf("get serial devices: %w", err)
}
ports = ps
}
// Check ports
SearchLoop:
for _, p := range ports {
m.logger.Printf("Checking port %s ...\n", p)
if err := m.checkPort("/dev/" + p); err != nil {
m.logger.Printf("\x1b[38;2;255;0;0mCheck failed: %s\x1b[38;2;255;255;255m\n", err.Error())
continue SearchLoop
}
m.logger.Print("Found modem on port: ", p)
m.port = at.New(m.logger, "/dev/"+p, m.baudrate)
return nil
}
return nil
}
func (m *modem) ping() error { func (m *modem) ping() error {
resp, err := m.port.Send("AT") resp, err := m.port.Send("AT")
if err != nil { if err != nil {
return err return fmt.Errorf("AT request: %w", err)
} }
if !resp.Check() { if !resp.Check() {
return fmt.Errorf("connection lost") return fmt.Errorf("AT request: error response: %s", resp)
} }
return nil return nil
} }
func getTtyDevices() ([]string, error) { func (m *modem) searchPort(isSoft bool) ([]string, error) {
// Get ports
ports, err := getTtyPorts(isSoft)
if err != nil {
fmt.Errorf("get devices: %w", err)
}
// Check ports
return m.getAtPorts(ports)
}
func (m *modem) getAtPorts(ports []string) ([]string, error) {
outPorts := make([]string, 0)
for _, p := range ports {
m.logger.Printf("Checking port %s ...\n", p)
if err := m.checkPort("/dev/" + p); err != nil {
m.logger.Printf("\x1b[38;2;255;0;0mCheck failed: %s\x1b[38;2;255;255;255m\n", err.Error())
continue
}
m.logger.Print("Found AT port: ", p)
outPorts = append(outPorts, "/dev/"+p)
}
return outPorts, nil
}
func getTtyPorts(isSoft bool) ([]string, error) {
if isSoft {
return ttyPorts, nil
}
// Get ports // Get ports
/**/ /**/
out, err := exec.Command("ls", "--", "/dev/tty[!0-9]*").Output() out, err := exec.Command("ls", "/dev/tty[!0-9]*").Output()
if err != nil { if err != nil {
return nil, fmt.Errorf("execute ls command: %w", err) return nil, fmt.Errorf("execute ls command: %w", err)
} }

View File

@ -1,69 +0,0 @@
package sms
import (
"fmt"
"strconv"
)
func DecodeError(code int) string {
switch code {
case 300:
return "ME failure"
case 301:
return "SMS service of ME reserved"
case 302:
return "Operation not allowed"
case 303:
return "Operation not supported"
case 304:
return "Invalid PDU mode parameter"
case 305:
return "Invalid text mode parameter"
case 310:
return "SIM not inserted"
case 311:
return "SIM PIN required"
case 312:
return "PH-SIM PIN required"
case 313:
return "SIM failure"
case 314:
return "SIM busy"
case 315:
return "SIM wrong"
case 316:
return "SIM PUK required"
case 317:
return "SIM PIN2 required"
case 318:
return "SIM PUK2 required"
case 320:
return "Memory failure"
case 321:
return "Invalid memory index"
case 322:
return "Memory full"
case 330:
return "SMSC address unknown"
case 331:
return "No network service"
case 332:
return "Network timeout"
case 340:
return "NO +CNMA ACK EXPECTED"
case 341:
return "Buffer overflow"
case 342:
return "SMS size more than expected"
case 500:
return "Unknown error"
}
return "UNDEFINED ERROR CODE"
}
func GetError(msg []byte) (int, error) {
if len(msg) >= len("+CMS ERROR: ")+3 && string(msg[:len("+CMS ERROR: ")]) == "+CMS ERROR: " {
return strconv.Atoi(string(msg[len("+CMS ERROR: ") : len("+CMS ERROR: ")+3]))
}
return 0, fmt.Errorf("failed to parse error")
}

88
api/modem/sms/setup.go Normal file
View File

@ -0,0 +1,88 @@
package sms
import (
"fmt"
"strconv"
"strings"
)
func (d *dialer) setupMsgSt() error {
// Check for free space for messages
// !!! I use one! memory for all three bindings
// 1: read and delete
// 2: sending
// 3: write received
// First try SM
if _, err := d.port.Send(`AT+CPMS="SM","SM","SM"`); err != nil {
return fmt.Errorf(`AT+CPMS="SM","SM","SM" request: %w`, err)
}
st, err := d.getCurMsgStSize()
if err != nil {
return fmt.Errorf("SM: %w", err)
}
if st[0].Used < st[0].Total {
d.logger.Printf("Use SM message storage: %d/%d\n", st[0].Used, st[0].Total)
return nil // There is space
}
d.logger.Printf("SM message storage is full: %d/%d\n", st[0].Used, st[0].Total)
// Second try ME
if _, err := d.port.Send(`AT+CPMS="ME","ME","ME"`); err != nil {
return fmt.Errorf(`AT+CPMS="ME","ME","ME" request: %w`, err)
}
st, err = d.getCurMsgStSize()
if err != nil {
return fmt.Errorf("ME: %w", err)
}
if st[0].Used < st[0].Total {
d.logger.Printf("Use ME message storage: %d/%d\n", st[0].Used, st[0].Total)
return nil // There is space
}
d.logger.Printf("ME message storage is full: %d/%d\n", st[0].Used, st[0].Total)
// Otherwise error
return fmt.Errorf("all storages are full")
}
// Message storage
type msgSt struct {
Name string
Used int
Total int
}
// Get size of used and total mem of current memory storage
func (d *dialer) getCurMsgStSize() ([]msgSt, error) {
// Request
resp, err := d.port.Send("AT+CPMS?")
if err != nil {
return nil, fmt.Errorf("AT+CPMS? request: %w", err)
}
// Check start and end
if !resp.Check() && !resp.CheckFront("+CPMS:") {
return nil, fmt.Errorf("AT+CPMS")
}
// Remove front and cut to values
resp = resp.RmFront("+CPMS:")
values := strings.Split(strings.ReplaceAll(strings.Split(resp.String(), "\n")[0], "\r", ""), ",")
if len(values) != 9 {
return nil, fmt.Errorf("CPMS response: invalid values count: [%s]", values)
}
// Parse values
outMsgs := [3]msgSt{}
for i := 0; i < 3; i++ {
name := values[i]
used, err := strconv.Atoi(values[i*3+1])
if err != nil {
return nil, fmt.Errorf("parse value #%d: %w", i+1, err)
}
total, err := strconv.Atoi(values[i*3+2])
if err != nil {
return nil, fmt.Errorf("parse value #%d, %w", i+2, err)
}
outMsgs[i] = msgSt{name, used, total}
}
return outMsgs[:], nil
}

View File

@ -6,7 +6,8 @@ import (
"log" "log"
"strings" "strings"
"github.com/CGSG-2021-AE4/modem-test/api/modem/at" "gitea.unprism.ru/KRBL/sim-modem/api/modem/at"
"gitea.unprism.ru/KRBL/sim-modem/api/modem/utils"
) )
type dialer struct { type dialer struct {
@ -14,14 +15,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,
@ -29,42 +31,66 @@ func New(logger *log.Logger, port at.Port) Dialer {
} }
func (d *dialer) Init() error { func (d *dialer) Init() error {
// Ensure serial port // Ensure serial port is connected
if !d.port.IsConnected() { if !d.port.IsConnected() {
return fmt.Errorf("serial port is not connected") return fmt.Errorf("serial port is not connected")
} }
// Ensure text format // Check ping
if resp, err := d.port.Send("AT+CMGF=1"); err != nil || !resp.Check() { if err := utils.CheckPIN(d.port, d.logger); err != nil {
return fmt.Errorf("check PIN: %w", err)
}
// Setup prefered message storage
// if err := d.setupMsgSt(); err != nil { // Does not use store now
// d.logger.Printf("ERROR setup msg storage: %s\n", err.Error())
// }
// Set notifications into console
if resp, err := d.port.Send("AT+CNMI=2,2"); err != nil || !resp.Check() {
if err != nil { if err != nil {
return fmt.Errorf("AT+CMGF=1 request: %w", err) return err
} }
return fmt.Errorf("failed to set SMS format") return fmt.Errorf("CNMI= error response: %s", resp.String())
}
// Check number
if resp, err := d.port.Send("AT+CNUM"); err != nil || !resp.Check() {
if err != nil {
return err
}
return fmt.Errorf("CNUM error response: %s", resp.String())
} }
return nil return nil
} }
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 sresp, err := d.port.Send(fmt.Sprintf(`AT+CMGSEX="%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
if err != nil { if err != nil {
return err
}
d.logger.Println(sresp)
// Message body
if err := d.port.RawSend(fmt.Sprintf("%s\x1A", msg)); err != nil {
return fmt.Errorf("message request: %w", err) return fmt.Errorf("message request: %w", err)
} }
if at.Resp(resp).Check() { resp, err := d.port.RawRead(at.ReadTimeout)
return nil
}
errCode, err := GetError([]byte(resp))
if err != nil { if err != nil {
return fmt.Errorf("send sms failed and failed to get error: %w", err) return fmt.Errorf("message request read: %w", err)
} }
return fmt.Errorf("failed to send with SMS error: %d - %s", errCode, DecodeError(errCode)) d.logger.Println("Send response:", resp)
if !at.Resp(resp).Check() {
return fmt.Errorf("error response: %s", resp)
}
return nil
} }
// Reads all new messages // Reads all new messages
func (d *dialer) ReadNew() ([]string, error) { func (d *dialer) ReadNew() ([]string, error) {
resp, err := d.port.Send("AT+CMGL") resp, err := d.port.Send("AT+CMGL=\"ALL\"")
if err != nil { if err != nil {
return nil, fmt.Errorf("AT+CMGL request: %w", err) return nil, err
} }
d.logger.Println("raw sms:", resp)
msgs := strings.Split(strings.Replace(string(resp), "\r", "", -1), "\n") msgs := strings.Split(strings.Replace(string(resp), "\r", "", -1), "\n")
outMsgs := make([]string, 0) outMsgs := make([]string, 0)

View File

@ -0,0 +1,37 @@
package utils
import (
"fmt"
"log"
"strings"
"gitea.unprism.ru/KRBL/sim-modem/api/modem/at"
)
func CheckService(port at.Port, logger *log.Logger) (bool, error) {
srv, err := getService(port)
if err != nil {
return false, fmt.Errorf("get service: %w", err)
}
if srv == "NO SERVICE" {
return false, nil
}
logger.Println("Current service:", srv)
return true, nil
}
// Returns service
func getService(port at.Port) (string, error) {
resp, err := port.Send("AT+CPSI?")
if err != nil {
return "", err
}
if !resp.Check() || !resp.CheckFront("+CPSI: ") {
return "", fmt.Errorf("error response: %s", resp)
}
values := strings.Split(strings.ReplaceAll(strings.Split(resp.RmFront("+CPSI: ").String(), "\n")[0], "\r", ""), ",")
if len(values) < 2 {
return "", fmt.Errorf("invalid values(len): [% s]", values)
}
return values[0], nil
}

109
api/modem/utils/signal.go Normal file
View File

@ -0,0 +1,109 @@
package utils
import (
"fmt"
"log"
"strconv"
"strings"
"gitea.unprism.ru/KRBL/sim-modem/api/modem/at"
)
func CheckSignal(port at.Port, logger *log.Logger) (bool, error) {
rssi, ber, err := getSignalQuality(port)
if err != nil {
return false, fmt.Errorf("get signal quality: %w", err)
}
logger.Printf("check signal: rssi=%d ber=%d\n", rssi, ber)
if err := checkRssi(rssi); err != nil {
return false, nil
}
if err := checkBer(ber); err != nil {
logger.Printf("bad ber(not critical): %s", err.Error()) // Happened not to be critical
}
return true, nil
}
func checkRssi(rssi int) error {
// rssi - Received signal strength indicator
// 0 -113 dBm or less
// 1 -111 dBm
// 2...30 -109... - 53 dBm
// 31 -51 dBm or greater
// 99 not known or not detectable
// 100 -116 dBm or less
// 101 -115 dBm
// 102…191 -114... - 26dBm
// 191 -25 dBm or greater
// 199 not known or not detectable
// 100…199 expand to TDSCDMA, indicate RSCPreceived
if rssi <= 2 { // Too poor
return fmt.Errorf("too poor <= -109dBm")
}
if rssi > 2 && rssi <= 31 {
return nil // Can live
}
if rssi == 99 {
return fmt.Errorf("not known or not detectable")
}
if rssi >= 100 && rssi <= 102 {
return fmt.Errorf("too poor <= -114dBm")
}
if rssi > 102 && rssi <= 191 {
return nil // Can live
}
if rssi == 199 {
return fmt.Errorf("not known or not detectable")
}
return fmt.Errorf("invalid code %d", rssi)
}
func checkBer(ber int) error {
// ber - Bit error rate
// 0 <0.01%
// 1 0.01% --- 0.1%
// 2 0.1% --- 0.5%
// 3 0.5% --- 1.0%
// 4 1.0% --- 2.0%
// 5 2.0% --- 4.0%
// 6 4.0% --- 8.0%
// 7 >=8.0%
// 99 not known or not detectable
if ber >= 0 && ber <= 3 {
// ber -> [0%;1%)
// Ok, can live
return nil
}
if ber >= 4 && ber <= 7 {
return fmt.Errorf("too high: %d code", ber)
}
if ber == 99 {
return fmt.Errorf("not known or not detectable")
}
return fmt.Errorf("invalid code %d", ber)
}
// Returns rssi and ber(look above)
func getSignalQuality(port at.Port) (int, int, error) {
resp, err := port.Send("AT+CSQ")
if err != nil {
return 99, 99, err
}
if !resp.Check() || !resp.CheckFront("+CSQ: ") {
return 99, 99, fmt.Errorf("error response: %s", resp)
}
values := strings.Split(strings.ReplaceAll(strings.Split(resp.RmFront("+CSQ: ").String(), "\n")[0], "\r", ""), ",")
if len(values) != 2 {
return 99, 99, fmt.Errorf("invalid values(len): [% s]", values)
}
rssi, err := strconv.Atoi(values[0])
if err != nil {
return 99, 99, fmt.Errorf("parse rssi: %w", err)
}
ber, err := strconv.Atoi(values[1])
if err != nil {
return 99, 99, fmt.Errorf("parse ber: %w", err)
}
return rssi, ber, nil
}

26
api/modem/utils/sim.go Normal file
View File

@ -0,0 +1,26 @@
package utils
import (
"fmt"
"log"
"strings"
"gitea.unprism.ru/KRBL/sim-modem/api/modem/at"
)
func CheckPIN(port at.Port, logger *log.Logger) error {
// Get code
resp, err := port.Send("AT+CPIN?")
if err != nil {
return fmt.Errorf("AT+CPIN? request: %w", err)
}
if !resp.Check() || !resp.CheckFront("+CPIN:") {
return fmt.Errorf("AT+CPIN? error response: %s", resp)
}
code := strings.ReplaceAll(strings.ReplaceAll(strings.Split(resp.RmFront("+CPIN:").String(), "\n")[0], "\r", ""), " ", "")
if code != "READY" {
return fmt.Errorf("not READY code: %s", code)
}
logger.Println("PIN is ready")
return nil
}

2
go.mod
View File

@ -1,4 +1,4 @@
module github.com/CGSG-2021-AE4/modem-test module gitea.unprism.ru/KRBL/sim-modem
go 1.22.5 go 1.22.5

193
main.go
View File

@ -1,43 +1,200 @@
package main package main
import ( import (
"context"
"fmt"
"log" "log"
"os" "os"
"os/signal"
"syscall"
"time"
"github.com/CGSG-2021-AE4/modem-test/api/modem" "gitea.unprism.ru/KRBL/sim-modem/api/modem"
) )
func main() { func main() {
log.Println("CGSG forever!!!") log.Println("CGSG forever!!!")
if err := mainE(); err != nil {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
// Just for logs
go func(ctx context.Context) {
<-ctx.Done()
log.Println("GOT INTERUPT SIGNAL")
}(ctx)
if err := mainE(ctx); err != nil {
log.Println("MAIN finished with error:", err.Error()) log.Println("MAIN finished with error:", err.Error())
} }
log.Println("END") log.Println("END")
} }
func mainE() error { var m modem.Modem
m := modem.New(log.New(os.Stdout, "modem:", log.LstdFlags)) var logger *log.Logger
log.Println("||||||||||||||||| INIT |||||||||||||||")
if err := m.Init(); err != nil { func InetInit() error {
return err // Connect to internet
if err := m.Ic().Connect(); err != nil {
return fmt.Errorf("connect to internet: %w", err)
} }
if !m.Validate() {
log.Println("AAAAAAAAAAAAAAA Validation failed") // Setup route table
// Linux now always manage to add ppp0 interface to route table in time so it is better to add ф loop here
setupRouteTableLoop:
for {
if err := m.Ic().SetDefaultRouteTable(); err != nil {
logger.Println("set route table:", err.Error())
time.Sleep(2 * time.Second)
} else {
break setupRouteTableLoop
}
}
return nil
}
func Cmd(cmd string) {
resp, err := m.At().SendWithTimeout(cmd, 10*time.Second) //50*time.Millisecond)
logger.Println(cmd, "===>", resp, err)
}
func mainE(ctx context.Context) error {
logger = log.New(os.Stdout, "main : ", log.LstdFlags)
m = modem.New(log.New(logger.Writer(), "modem : ", log.LstdFlags))
logger.Println("||||||||||||||||| INIT |||||||||||||||")
// If power is down modem won't find suitable devices add will try to send powerOn signal and then try again
initLoop:
for {
select {
case <-ctx.Done(): // For interupt
logger.Println("Break init loop")
return nil
default:
if err := m.Init(); err != nil {
logger.Println("Init ended with error:", err.Error())
// logger.Println("Turn on...")
// if err := m.PowerOnCtx(ctx); err != nil {
// logger.Println("Turn on error:", err.Error())
// }
time.Sleep(time.Second)
continue initLoop
}
break initLoop
}
}
// Final check for sure
if !m.IsConnected() {
logger.Println("Modem is not connected")
return nil return nil
} }
log.Println("||||||||||||||||| GET INFO |||||||||||||||||") // Close() deinits everything recursively
log.Println(m.GetInfo()) defer func() {
logger.Println("||||||||||||||||| CLOSE |||||||||||||||")
m.Close()
}()
log.Println("||||||||||||||||| SEND SMS |||||||||||||||||") // Internet connect
log.Println(m.At().Send("AT+CNUM")) // if err := InetInit(); err != nil {
// if err := m.Sms().Send("+79218937173", "CGSG forever"); err != nil {
// return err // return err
// } // }
if ms, err := m.Sms().ReadNew(); err != nil {
return err logger.Println("||||||||||||||||| SMS |||||||||||||||||")
} else {
log.Println("NEW:", ms) // Select ME PMS
// logger.Println("SEND SMS")
// logger.Println(m.Sms().Send("+79218937173", "CGSG forever!!!"))
// m.At().RawSend("\r\n\x1A\r\n")
// Cmd("AT+CREG?")
// Cmd("AT+CNMI?")
Cmd("AT+CNETSCAN")
// Cmd("AT+CSCA?")
// Cmd("AT+CPOL?")
// Cmd("AT+COPS?")
// // Cmd("AT+COPS=?")
// Cmd("AT+CPSI?")
// resp, err = m.At().Send("AT+CNMI=2,2")
for {
select {
case <-ctx.Done():
logger.Println("Break main loop")
return nil
default:
// Cmd("AT+CPSI?")
// Cmd("AT+CIPGSMLOC=1")
// Cmd("AT+CSQ")
// Cmd("AT+CTZU?")
// Cmd("AT+CPIN?")
// Cmd("AT+CCLK?")
// logger.Println(m.Gps().GetStatus())
// m.Update()
// st, _ := m.Gps().GetStatus()
// logger.Printf("FOUND SATELITES: %d\n", st.FoundSatelitesCount)
// data := m.GetData()
// logger.Printf("GPS DATA: %f%s %f%s\n", data.Latitude, data.LatitudeIndicator, data.Longitude, data.LongitudeIndicator)
// logger.Println(m.GetTime())
// logger.Println(m.Ic().Ping())
time.Sleep(time.Second)
}
// Cmd("AT+CSQ")
// Cmd("AT+COPS?")
// if err := m.CheckSignal(); err != nil {
// logger.Println(err)
// } else {
// logger.Println("AAAAAAAAAAA THERE IS SIGNAL")
// }
// readLen, err := m.At().SerialPort().Read(buf)
// if err != nil {
// return err
// }
// if readLen > 0 {
// logger.Println(string(buf[:readLen]))
// }
} }
// resp, err = m.At().Send("AT+CPMS?")
// logger.Println("Prefered mem storage:", resp, err)
// resp, err = m.At().Send("AT+CREG?")
// logger.Println("Network registration:", resp, err)
// resp, err = m.At().Send("AT+CPMS?")
// logger.Println("Prefered mem storage:", resp, err)
// resp, err = m.At().Send("AT+CPMS=?")
// logger.Println("Possible mem storage:", resp, err)
// resp, err = m.At().Send("AT+CNMI?")
// logger.Println("New message indications:", resp, err)
// resp, err = m.At().Send("AT+CMGL=\"REC UNREAD\"")
// logger.Println("New messages:", resp, err)
// resp, err = m.At().Send("AT+CNMI=2,1")
// logger.Println("AT+CNMI=2,1:", resp, err)
// resp, err = m.At().Send("AT+CNMI?")
// logger.Println("New message indications:", resp, err)
// logger.Println("Reading port...")
// for {
// readLen, err := m.At().SerialPort().Read(buf)
// if err != nil {
// return err
// }
// if readLen > 0 {
// logger.Println(string(buf[:readLen]))
// }
// }
// for {
// resp, err = m.At().Send("AT+CSQ")
// logger.Println("AT+CSQ:", resp, err)
// time.Sleep(500 * time.Millisecond)
// }
// logger.Println("||||||||||||||||| Checking gps status |||||||||||||||||")
// st, err := m.Gps().GetStatus()
// if err != nil {
// return err
// }
// logger.Printf("GPS Status:%+v\n", st)
// logger.Println("Turn off", m.PowerOff())
return nil return nil
} }

View File

@ -1,2 +0,0 @@
ISSUES:
- create input buffer every at port creation