Compare commits

...

8 Commits

Author SHA1 Message Date
Andrey Egorov
4f89f15146 Fix: latitude, longitude format 2024-09-30 20:43:37 +03:00
Andrey Egorov
509006fae1 Add: power on with context, SIM808 support 2024-08-19 17:07:46 +03:00
Andrey Egorov
3f5412fac0 Add: internet default routes 2024-08-15 16:47:20 +03:00
Andrey Egorov
5065cfdfca Added GetTime. Added independent internet port. Debugged internet connection. 2024-08-12 19:24:31 +03:00
Andrey Egorov
5059814d39 Added service checks. Some refactoring. 2024-08-12 13:40:26 +03:00
Andrey Egorov
d21ad0ce00 Added signal check. 2024-08-10 15:21:27 +03:00
Andrey Egorov
75d05f197c Improved SMS. 2024-08-09 18:17:20 +03:00
Andrey Egorov
b3fb13b7b3 Removed internet and sms temporarily. 2024-08-09 11:42:55 +03:00
17 changed files with 1143 additions and 421 deletions

View File

@ -4,4 +4,4 @@ export GOARM=6
export CGO_ENABLED=0 export CGO_ENABLED=0
build: build:
@go build -o out/modem main.go @go build -o out/out main.go

View File

@ -37,8 +37,10 @@ type Port interface {
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
} }
@ -105,34 +107,73 @@ func (p *atPort) IsConnected() bool {
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() p.mutex.Lock()
defer p.mutex.Unlock() 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

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

@ -11,10 +11,10 @@ import (
) )
type Data struct { type Data struct {
Latitude float64 `json:"Latitude"` Latitude float64 `json:"Latitude"` // ddmm.mmmmmm
Longitude float64 `json:"Longitude"` Longitude float64 `json:"Longitude"` // dddmm.mmmmmm
LatitudeIndicator string `json:"Latitude_indicator"` // North/South LatitudeIndicator string `json:"Latitude_indicator"` // N/S - North/South
LongitudeIndicator string `json:"Longitude_indicator"` // West/East LongitudeIndicator string `json:"Longitude_indicator"` // W/E - West/East
Speed float64 `json:"Speed"` Speed float64 `json:"Speed"`
Course float64 `json:"-"` Course float64 `json:"-"`
Altitude float64 `json:"-"` Altitude float64 `json:"-"`
@ -59,8 +59,6 @@ func (gps *Data) decode(str string) error {
if err != nil { if err != nil {
logger.Println("ERROR parse longitude:", err.Error()) logger.Println("ERROR parse longitude:", err.Error())
} }
newGpsInfo.Latitude /= 100
newGpsInfo.Longitude /= 100
newGpsInfo.LatitudeIndicator = strs[1] newGpsInfo.LatitudeIndicator = strs[1]
newGpsInfo.LongitudeIndicator = strs[3] newGpsInfo.LongitudeIndicator = strs[3]
newGpsInfo.Date = strs[4] newGpsInfo.Date = strs[4]

View File

@ -85,7 +85,7 @@ func (g *gps) switchGpsMode(on bool) error {
// Check gps mode status // Check gps mode status
resp, err := g.port.Send("AT+CGPS?") resp, err := g.port.Send("AT+CGPS?")
if err != nil { if err != nil {
return fmt.Errorf("make at ask: %w", err) return err
} }
if !resp.Check() || !resp.CheckFront("+CGPS:") { if !resp.Check() || !resp.CheckFront("+CGPS:") {
return fmt.Errorf("get GPS mode: error response: %s", resp) return fmt.Errorf("get GPS mode: error response: %s", resp)
@ -98,7 +98,7 @@ func (g *gps) switchGpsMode(on bool) error {
// Modem is not in GPS mode // Modem is not in GPS mode
resp, err = g.port.Send("AT+CGPS=" + onStr) resp, err = g.port.Send("AT+CGPS=" + onStr)
if err != nil { if err != nil {
return fmt.Errorf("set GPS mode: %w", err) return err
} }
if !resp.Check() { if !resp.Check() {
return fmt.Errorf("set GPS mode: error response: %s", resp) return fmt.Errorf("set GPS mode: error response: %s", resp)

View File

@ -34,12 +34,14 @@ const (
) )
func secondCountDownTimer(title string, logger *log.Logger, t time.Duration) { func secondCountDownTimer(title string, logger *log.Logger, t time.Duration) {
_ = title
_ = logger
counter := 0 counter := 0
for { for {
if counter > int(t.Seconds()) { if counter > int(t.Seconds()) {
break break
} }
logger.Printf("%s: %d/%f\n", title, counter, t.Seconds()) // logger.Printf("%s: %d/%f\n", title, counter, t.Seconds())
time.Sleep(time.Second) time.Sleep(time.Second)
counter += 1 counter += 1
} }
@ -56,7 +58,7 @@ func (g *gps) rawCollect(flags nmeaFlags) (string, error) {
// Set output rate to 10Hz // Set output rate to 10Hz
if resp, err := g.port.Send("AT+CGPSNMEARATE=1"); err != nil { if resp, err := g.port.Send("AT+CGPSNMEARATE=1"); err != nil {
return "", fmt.Errorf("AT+CGPSNMEARATE= request: %w", err) return "", err
} else { } else {
if !resp.Check() { if !resp.Check() {
return "", fmt.Errorf("set output rate: error response: %s", resp) return "", fmt.Errorf("set output rate: error response: %s", resp)
@ -118,7 +120,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

View File

@ -42,14 +42,14 @@ checkLoop:
} }
st.GotResponses = true st.GotResponses = true
g.logger.Println("NMEA check:", s) // g.logger.Println("NMEA check:", s)
values := strings.Split(s, ",") values := strings.Split(s, ",")
if len(values[0]) != 6 { if len(values[0]) != 6 {
return StatusNil, fmt.Errorf("nmea invalid sentence: %s", s) return StatusNil, fmt.Errorf("nmea invalid sentence: %s", s)
} }
switch values[0][3:] { // Switch by content switch values[0][3:] { // Switch by content
case "GSV": // Any satelites case "GSV": // Any satelites
g.logger.Println("check GSV") // g.logger.Println("check GSV")
// Check len // Check len
if len(values) < 17 { if len(values) < 17 {
g.logger.Println("GSV too small values") g.logger.Println("GSV too small values")
@ -83,7 +83,7 @@ checkLoop:
} }
st.FoundSatelitesCount = satCount st.FoundSatelitesCount = satCount
case "GSA": // Active satelites case "GSA": // Active satelites
g.logger.Println("check GSA") // g.logger.Println("check GSA")
// Check len // Check len
if len(values) < 17 { if len(values) < 17 {
@ -107,7 +107,7 @@ checkLoop:
} }
st.ActiveSatelitesCount = max(st.ActiveSatelitesCount, count) st.ActiveSatelitesCount = max(st.ActiveSatelitesCount, count)
case "RMC": // Minimum GPS data case "RMC": // Minimum GPS data
g.logger.Println("check RMC") // g.logger.Println("check RMC")
// Check len // Check len
if len(values) < 12 { if len(values) < 12 {
g.logger.Println("RMC too small values") g.logger.Println("RMC too small values")
@ -125,7 +125,7 @@ checkLoop:
st.IsValidData = true st.IsValidData = true
} }
case "GST": case "GST":
g.logger.Println("check GST") // g.logger.Println("check GST")
// Check len // Check len
if len(values) < 8 { if len(values) < 8 {
g.logger.Println("GST too small values") g.logger.Println("GST too small values")

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"
"gitea.unprism.ru/KRBL/sim-modem/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,189 +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("validate SIM: error response: %s", resp)
}
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("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 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)
}
// 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")
} }
c.logger.Println("Ping 2 resp:", string(resp))
return !(strings.Contains(string(resp), "Destination Host Unreachable") || strings.Contains(string(resp), "Destination Net Unreachable")) // tmp solution func (c *conn) PingDefault() error {
return c.ping([]string{"-c", strconv.Itoa(pingPacketsCount), "-w", strconv.Itoa(pingTimeout), pingAddr}, pingTimeout)
}
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,11 +1,13 @@
package modem package modem
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -17,6 +19,12 @@ import (
"gitea.unprism.ru/KRBL/sim-modem/api/modem/sms" "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"`
gps.Data gps.Data
@ -31,6 +39,7 @@ type modem struct {
baudrate int baudrate int
deviceName string deviceName string
port at.Port port at.Port
model string
// Gpio values // Gpio values
onOffPin gpio.Pin // For turning on and off onOffPin gpio.Pin // For turning on and off
@ -53,14 +62,17 @@ type Modem interface {
IsConnected() bool IsConnected() bool
Update() error Update() error
GetData() ModemData GetData() ModemData
GetTime() (time.Time, error)
PowerOn() error PowerOn() error
PowerOnCtx(ctx context.Context) error // Because it takes ~30 seconds
PowerOff() error PowerOff() error
// Access to SMS, GPS, AT interfaces mostly for debug // Access to SMS, GPS, AT interfaces mostly for debug
At() at.Port // Send At() at.Port // Send
Gps() gps.Gps // Update, GetData, GetStatus Gps() gps.Gps // Update, GetData, GetStatus
Sms() sms.Sms // Send, ReadNew Sms() sms.Sms // Send, ReadNew
Ic() internet.Conn // Connect, Disconnect
io.Closer io.Closer
} }
@ -80,29 +92,41 @@ func (m *modem) Init() error {
defer m.mutex.Unlock() 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)
} }
// 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)
} }
if len(ports) == 0 {
// Wide search // Wide search
if m.port == nil { ports, err := m.searchPort(true)
if err := m.searchPort(false); err != nil { if err != nil {
return fmt.Errorf("not soft port search: %w", err) 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)
} }
@ -113,18 +137,18 @@ func (m *modem) Init() error {
m.logger.Println("=============================== Init submodules") m.logger.Println("=============================== Init submodules")
m.ic = internet.New(log.New(submodulesLogger, "modem-internet : ", log.LstdFlags), m.port) m.ic = internet.New(log.New(submodulesLogger, "modem-internet : ", log.LstdFlags), m.port)
if err := m.ic.Init(); err != nil { 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()) m.logger.Printf("\x1b[38;2;255;0;0mInternet: %s\x1b[38;2;255;255;255m\n", err.Error())
} else { } else {
m.logger.Println("\x1b[38;2;0;255;0mInternet OK\x1b[38;2;255;255;255m") 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) // 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 {
m.logger.Printf("\x1b[38;2;255;0;0mSMS: %s\x1b[38;2;255;255;255m\n", err.Error()) // m.logger.Printf("\x1b[38;2;255;0;0mSMS: %s\x1b[38;2;255;255;255m\n", err.Error())
} else { // } else {
m.logger.Println("\x1b[38;2;0;255;0mSMS OK\x1b[38;2;255;255;255m") // m.logger.Println("\x1b[38;2;0;255;0mSMS OK\x1b[38;2;255;255;255m")
} // }
m.gps = gps.New(log.New(submodulesLogger, "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 {
@ -170,16 +194,58 @@ func (m *modem) GetData() ModemData {
} }
} }
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 { func (m *modem) PowerOn() error {
m.onOffPin.PowerOn() // DEBUG do not want to wait 30 seconds m.onOffPin.PowerOn() // DEBUG do not want to wait 30 seconds
return nil 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 { func (m *modem) PowerOff() error {
_, err := m.At().Send("AT+CPOF") _, err := m.At().Send("AT+CPOF")
return err return err
} }
func (m *modem) restart() error {
m.PowerOff()
time.Sleep(10 * time.Second)
m.PowerOn()
return nil
}
func (m *modem) Sms() sms.Sms { func (m *modem) Sms() sms.Sms {
return m.sms return m.sms
} }
@ -192,24 +258,38 @@ func (m *modem) At() at.Port {
return m.port return m.port
} }
func (m *modem) Ic() internet.Conn {
return m.ic
}
func (m *modem) Close() error { // I can't return error so I log it func (m *modem) Close() error { // I can't return error so I log it
m.mutex.Lock() m.mutex.Lock()
defer m.mutex.Unlock() defer m.mutex.Unlock()
// Close submodules
if m.sms != nil {
if err := m.sms.Close(); err != 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()) m.logger.Printf("\x1b[38;2;255;0;0mclose sms error: %s\x1b[38;2;255;255;255m\n", err.Error())
} }
if err := m.port.Close(); err != nil {
m.logger.Printf("\x1b[38;2;255;0;0mclose serial port 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())
}
}
// Close gpio and serial
if err := m.onOffPin.Close(); err != nil { if err := m.onOffPin.Close(); err != nil {
m.logger.Printf("\x1b[38;2;255;0;0mclose gpio pin error: %s\x1b[38;2;255;255;255m\n", err.Error()) 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 {
m.logger.Printf("\x1b[38;2;255;0;0mclose internet connection error: %s\x1b[38;2;255;255;255m\n", err.Error()) 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
} }
@ -269,10 +349,10 @@ func (m *modem) saveGPS(path string) error {
// Short way to send command // Short way to send command
func (m *modem) printCmd(cmd string) { func (m *modem) printCmd(cmd string) {
if resp, err := m.port.Send(cmd); err != nil { if resp, err := m.port.Send(cmd); err != nil {
m.logger.Println("FAILED TO SEND REQ", cmd, ":", err.Error()) m.logger.Println("FAILED TO SEND REQ", cmd, "===>", err.Error())
} else { } else {
_ = resp _ = resp
// m.logger.Println("CMD", cmd, ":", resp) // m.logger.Println("CMD", cmd, "===>", resp)
} }
} }
@ -287,14 +367,46 @@ func (m *modem) setupPort() error {
return fmt.Errorf("reset output buffer: %w", err) return fmt.Errorf("reset output buffer: %w", err)
} }
// m.restart()
// These commands ensure that correct modes are set // 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("ATE0") // Sometimes enables echo mode
m.printCmd("AT+CGPSFTM=0") // Sometimes does not turn off nmea m.printCmd("AT+CGPSFTM=0") // Sometimes does not turn off nmea
m.printCmd("AT+CMEE=2") // Turn on errors describtion m.printCmd("AT+CMEE=2") // Turn on errors describtion
m.printCmd("AT+CTZU=1") // Turn on time update
return nil 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
@ -315,9 +427,12 @@ 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...
// m.restart()
// To filter dead ports // To filter dead ports
if _, err := m.port.Send("AT"); err != nil {
return fmt.Errorf("ping error: %w", err) if err := m.checkCurPortDead(); err != nil {
return fmt.Errorf("echo: %w", err)
} }
if err := m.setupPort(); err != nil { if err := m.setupPort(); err != nil {
@ -344,43 +459,23 @@ func (m *modem) checkPort(portName string) (outErr error) {
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 {
@ -392,10 +487,39 @@ func (m *modem) ping() error {
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

@ -7,6 +7,7 @@ import (
"strings" "strings"
"gitea.unprism.ru/KRBL/sim-modem/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 {
@ -30,55 +31,66 @@ func New(logger *log.Logger, port at.Port) Sms {
} }
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")
} }
// Check ping
// Check SIM an PIN if err := utils.CheckPIN(d.port, d.logger); err != nil {
if resp, err := d.port.Send("AT+CPIN?"); err != nil || !resp.Check() { return fmt.Errorf("check PIN: %w", err)
if err != nil {
return fmt.Errorf("check pin: %w", err)
} }
return fmt.Errorf("check pin: error response: %s", resp) // 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 {
return err
}
return fmt.Errorf("CNMI= error response: %s", resp.String())
} }
// Ensure text format // Check number
d.logger.Println(d.port.Send("AT+CMGF")) if resp, err := d.port.Send("AT+CNUM"); err != nil || !resp.Check() {
if resp, err := d.port.Send("AT+CMGF=1"); err != nil || !resp.Check() {
if err != nil { if err != nil {
return fmt.Errorf("set to text format request: %w", err) return err
} }
return fmt.Errorf("set SIM format: error response: %s", resp) 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\n\r", 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)
} }
d.logger.Println("SEND RESPONSE:", resp) resp, err := d.port.RawRead(at.ReadTimeout)
resp, err = d.port.RawSend("\x1A")
if err != nil { if err != nil {
return fmt.Errorf("message request: %w", err) return fmt.Errorf("message request read: %w", err)
} }
d.logger.Println("SEND RESPONSE:", resp) d.logger.Println("Send response:", resp)
errCode, err := GetError([]byte(resp)) if !at.Resp(resp).Check() {
if err != nil { return fmt.Errorf("error response: %s", resp)
return fmt.Errorf("send sms failed and failed to get error: %w", err)
} }
return fmt.Errorf("failed to send with SMS error: %d - %s", errCode, DecodeError(errCode)) 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=\"UNREAD\"") 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
}

200
main.go
View File

@ -1,64 +1,198 @@
package main package main
import ( import (
"context"
"fmt"
"log" "log"
"os" "os"
"os/signal"
"syscall"
"time"
"gitea.unprism.ru/KRBL/sim-modem/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
logger := log.New(os.Stdout, "main : ", log.LstdFlags) var logger *log.Logger
m := modem.New(log.New(logger.Writer(), "modem : ", log.LstdFlags))
logger.Println("||||||||||||||||| INIT |||||||||||||||") func InetInit() error {
if err := m.Init(); err != nil { // Connect to internet
logger.Println("Init ended with error:", err.Error()) if err := m.Ic().Connect(); err != nil {
logger.Println("Try to turn on") return fmt.Errorf("connect to internet: %w", err)
if err := m.PowerOn(); err != nil {
return err
} }
logger.Println("Reinit")
if err := m.Init(); err != nil { // Setup route table
return err // 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
} }
} }
if !m.IsConnected() {
logger.Println("AAAAAAAAAAAAAAA Modem is not connected")
return nil return nil
} }
// m.PowerOff()
// time.Sleep(10 * time.Second)
// m.PowerOn()
logger.Println("||||||||||||||||| GET INFO |||||||||||||||||") func Cmd(cmd string) {
logger.Println(m.Update()) resp, err := m.At().SendWithTimeout(cmd, 10*time.Second) //50*time.Millisecond)
logger.Printf("DATA: %+v\n", m.GetData()) logger.Println(cmd, "===>", resp, err)
}
logger.Println("||||||||||||||||| SEND SMS |||||||||||||||||") func mainE(ctx context.Context) error {
logger.Println(m.At().Send("AT+CNUM")) logger = log.New(os.Stdout, "main : ", log.LstdFlags)
// if err := m.Sms().Send("+79218937173", "CGSG forever"); err != nil { 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
}
// Close() deinits everything recursively
defer func() {
logger.Println("||||||||||||||||| CLOSE |||||||||||||||")
m.Close()
}()
// Internet connect
// if err := InetInit(); err != nil {
// return err // return err
// } // }
if ms, err := m.Sms().ReadNew(); err != nil {
return err logger.Println("||||||||||||||||| SMS |||||||||||||||||")
} else {
logger.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)
} }
logger.Println("||||||||||||||||| Checking gps status |||||||||||||||||") // Cmd("AT+CSQ")
st, err := m.Gps().GetStatus() // Cmd("AT+COPS?")
if err != nil {
return err // 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]))
// }
} }
logger.Printf("GPS Status:%+v\n", st)
// 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()) // logger.Println("Turn off", m.PowerOff())