sim-modem/api/modem/modem.go
2024-08-19 17:07:46 +03:00

539 lines
13 KiB
Go

package modem
import (
"context"
"fmt"
"io"
"log"
"os"
"os/exec"
"strconv"
"strings"
"sync"
"time"
"gitea.unprism.ru/KRBL/sim-modem/api/modem/at"
"gitea.unprism.ru/KRBL/sim-modem/api/modem/gpio"
"gitea.unprism.ru/KRBL/sim-modem/api/modem/gps"
"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 {
Port string `json:"Port"`
gps.Data
}
type modem struct {
// Internal values
logger *log.Logger
mutex sync.Mutex
// Serial values
baudrate int
deviceName string
port at.Port
model string
// Gpio values
onOffPin gpio.Pin // For turning on and off
// Other values
lastUpdateTime time.Time
// GPS
gps gps.Gps
// Internet connection
ic internet.Conn
// Sms and calls
sms sms.Sms
}
type Modem interface {
Init() error
IsConnected() bool
Update() error
GetData() ModemData
GetTime() (time.Time, error)
PowerOn() error
PowerOnCtx(ctx context.Context) error // Because it takes ~30 seconds
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
}
func New(logger *log.Logger) Modem {
return &modem{
logger: logger,
baudrate: 115200,
onOffPin: gpio.New(log.New(logger.Writer(), "gpio", log.LstdFlags), 6),
lastUpdateTime: time.Now(),
}
}
func (m *modem) Init() error {
m.mutex.Lock()
defer m.mutex.Unlock()
// Turn module on
m.logger.Println("=============================== Init gpio")
if err := m.onOffPin.Init(); err != nil {
return fmt.Errorf("gpio pin init: %w", err)
}
// Search
m.logger.Println("=============================== Search")
ports, err := m.searchPort(true)
if err != nil {
return fmt.Errorf("soft port search: %w", err)
}
if len(ports) == 0 {
// Wide search
ports, err := m.searchPort(true)
if err != nil {
return fmt.Errorf("wide port search: %w", err)
}
if len(ports) == 0 {
return fmt.Errorf("no AT ports found")
}
}
if len(ports) == 1 {
// 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
m.logger.Println("=============================== Connect")
m.port = at.New(m.logger, ports[0], m.baudrate)
if err := m.connect(); err != nil {
return fmt.Errorf("connect: %w", err)
}
// Init submodules
submodulesLogger := m.logger.Writer() // FOR more logs
// submodulesLogger := io.Discard // FOR less logs
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")
}
// m.sms = sms.New(log.New(submodulesLogger, "modem-sms : ", log.LstdFlags), m.port)
// 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())
// } else {
// 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)
if err := m.gps.Init(); err != nil {
m.logger.Printf("\x1b[38;2;255;0;0mgps init %w\x1b[38;2;255;255;255m\n", err)
} else {
m.logger.Println("\x1b[38;2;0;255;0mGPS OK\x1b[38;2;255;255;255m")
}
// Tests
// 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 {
m.mutex.Lock()
defer m.mutex.Unlock()
if !m.IsConnected() {
m.logger.Println("No connection to module")
return nil
}
m.logger.Println("Update", m.gps)
if err := m.gps.Update(); err != nil {
m.logger.Println("gps update:", err.Error())
}
// Read new messages
return nil
}
func (m *modem) GetData() ModemData {
m.mutex.Lock()
defer m.mutex.Unlock()
return ModemData{
Port: m.port.GetName(),
Data: m.gps.GetData(),
}
}
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
}
func (m *modem) Gps() gps.Gps {
return m.gps
}
func (m *modem) At() at.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
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())
}
}
// Close gpio and serial
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())
}
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())
}
return nil
}
///////////// Private functions
func (m *modem) connect() error {
if m.port == nil {
return fmt.Errorf("port is not defined")
}
return m.port.Connect()
}
func (m *modem) disconnect() error {
if m.port == nil {
return fmt.Errorf("port is not defined")
}
return m.port.Disconnect()
}
func (m *modem) IsConnected() bool {
if m.port != nil {
return m.port.IsConnected()
}
return false
}
func (m *modem) testGPS() error {
m.logger.Println("Testing GPS")
if err := m.Update(); err != nil {
return fmt.Errorf("update: %w", err)
}
m.logger.Println("Current coords:", m.getShortInfo())
return nil
}
// Difference: I do not set \n at the end of string
func (m *modem) getShortInfo() string {
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 {
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return fmt.Errorf("open file: %w", err)
}
defer f.Close()
if _, err = f.WriteString(m.getShortInfo()); err != nil {
return fmt.Errorf("write file: %W", err)
}
return nil
}
// Short way to send command
func (m *modem) printCmd(cmd string) {
if resp, err := m.port.Send(cmd); err != nil {
m.logger.Println("FAILED TO SEND REQ", cmd, "===>", err.Error())
} else {
_ = resp
// m.logger.Println("CMD", cmd, "===>", resp)
}
}
// 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) {
defer func() {
if outErr != nil { // Clear port if there is error
m.port = nil
}
}()
// Check device for existance
if _, err := os.Stat(portName); err != nil {
return fmt.Errorf("device does not exist")
}
// Check serial connection
// Connect
m.port = at.New(m.logger, portName, m.baudrate)
if err := m.port.Connect(); err != nil {
return fmt.Errorf("connect: %w", err)
}
defer m.port.Disconnect() // Do not bother about errors...
// m.restart()
// To filter dead ports
if err := m.checkCurPortDead(); err != nil {
return fmt.Errorf("echo: %w", err)
}
if err := m.setupPort(); err != nil {
return fmt.Errorf("setup port: %w", err)
}
// Ping
m.logger.Println("Ping...")
if err := m.ping(); err != nil {
return fmt.Errorf("ping error: %w", err)
}
m.logger.Println("\x1b[38;2;0;255;0mOK\x1b[38;2;255;255;255m")
// Check model
m.logger.Println("Check model...")
resp, err := m.port.Send("AT+CGMM")
if err != nil {
return fmt.Errorf("get model: %w", err)
}
if !resp.Check() {
return fmt.Errorf("get model: error response: %s", resp)
}
model := strings.Split(resp.String(), "\n")[0]
if err != nil {
return fmt.Errorf("get model: %w", err)
}
// Check model
foundModel := ""
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")
return nil
}
func (m *modem) ping() error {
resp, err := m.port.Send("AT")
if err != nil {
return fmt.Errorf("AT request: %w", err)
}
if !resp.Check() {
return fmt.Errorf("AT request: error response: %s", resp)
}
return nil
}
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
/**/
out, err := exec.Command("ls", "/dev/tty[!0-9]*").Output()
if err != nil {
return nil, fmt.Errorf("execute ls command: %w", err)
}
ports := strings.Split(string(out), "\n")
return ports, nil
}
/*
TODOs:
maybe to store read/write buf in obj
QUESTIONS:
JSON why you clamp
*/
/*
*/