github.com/simpleiot/simpleiot@v0.18.3/network/modem.go (about)

     1  package network
     2  
     3  // this module currently supports the BG96 modem connected via USB
     4  
     5  import (
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"log"
    10  	"os/exec"
    11  	"strings"
    12  	"time"
    13  
    14  	nmea "github.com/adrianmo/go-nmea"
    15  	"github.com/jacobsa/go-serial/serial"
    16  	"github.com/simpleiot/simpleiot/data"
    17  	"github.com/simpleiot/simpleiot/file"
    18  	"github.com/simpleiot/simpleiot/respreader"
    19  )
    20  
    21  // APNVerizon is the APN to use on VZ network
    22  const APNVerizon = "vzwinternet"
    23  
    24  // APNKajeet is the APN to use on the Kajeet network
    25  const APNKajeet = "kajeet.gw12.vzwentp"
    26  
    27  // APNHologram is the APN to use on the Hologram network
    28  const APNHologram = "hologram"
    29  
    30  // Modem is an interface that always reports detected/connected
    31  type Modem struct {
    32  	iface      string
    33  	atCmdPort  io.ReadWriteCloser
    34  	lastPPPRun time.Time
    35  	config     ModemConfig
    36  	enabled    bool
    37  }
    38  
    39  // ModemConfig describes the configuration for a modem
    40  type ModemConfig struct {
    41  	ChatScript    string
    42  	AtCmdPortName string
    43  	Reset         func() error
    44  	Debug         bool
    45  	APN           string
    46  }
    47  
    48  // NewModem constructor
    49  func NewModem(config ModemConfig) *Modem {
    50  	ret := &Modem{
    51  		iface:  "ppp0",
    52  		config: config,
    53  	}
    54  
    55  	DebugAtCommands = config.Debug
    56  
    57  	return ret
    58  }
    59  
    60  func (m *Modem) openCmdPort() error {
    61  	if m.atCmdPort != nil {
    62  		// port is already open
    63  		return nil
    64  	}
    65  
    66  	if !m.detected() {
    67  		return errors.New("open failed, modem not detected")
    68  	}
    69  
    70  	options := serial.OpenOptions{
    71  		PortName:          m.config.AtCmdPortName,
    72  		BaudRate:          115200,
    73  		DataBits:          8,
    74  		StopBits:          1,
    75  		MinimumReadSize:   1,
    76  		RTSCTSFlowControl: true,
    77  	}
    78  
    79  	port, err := serial.Open(options)
    80  
    81  	if err != nil {
    82  		return err
    83  	}
    84  
    85  	m.atCmdPort = respreader.NewReadWriteCloser(port, 10*time.Second,
    86  		50*time.Millisecond)
    87  
    88  	return nil
    89  }
    90  
    91  // Desc returns description
    92  func (m *Modem) Desc() string {
    93  	return "modem"
    94  }
    95  
    96  // detected returns true if modem detected
    97  func (m *Modem) detected() bool {
    98  	return file.Exists("/dev/ttyUSB0") &&
    99  		file.Exists("/dev/ttyUSB1") &&
   100  		file.Exists("/dev/ttyUSB2") &&
   101  		file.Exists("/dev/ttyUSB3")
   102  }
   103  
   104  func (m *Modem) pppActive() bool {
   105  	if !m.detected() {
   106  		return false
   107  	}
   108  
   109  	_, err := GetIP(m.iface)
   110  	return err == nil
   111  }
   112  
   113  // Configure modem interface
   114  func (m *Modem) Configure() (InterfaceConfig, error) {
   115  	if !m.enabled {
   116  		return InterfaceConfig{}, errors.New("Configure error, modem disabled")
   117  	}
   118  
   119  	ret := InterfaceConfig{
   120  		Apn: m.config.APN,
   121  	}
   122  
   123  	// current sets APN and configures for internal SIM
   124  	if err := m.openCmdPort(); err != nil {
   125  		return ret, err
   126  	}
   127  
   128  	// disable echo as it messes up the respreader in that it
   129  	// echos the command, which is not part of the response
   130  
   131  	err := CmdOK(m.atCmdPort, "ATE0")
   132  	if err != nil {
   133  		return ret, err
   134  	}
   135  
   136  	err = CmdSetApn(m.atCmdPort, m.config.APN)
   137  	if err != nil {
   138  		return ret, err
   139  	}
   140  
   141  	mode, err := CmdBg96GetScanMode(m.atCmdPort)
   142  	fmt.Println("BG96 scan mode: ", mode)
   143  	if err != nil {
   144  		return ret, fmt.Errorf("Error getting scan mode: %v", err.Error())
   145  	}
   146  
   147  	if mode != BG96ScanModeLTE {
   148  		fmt.Println("Setting BG96 scan mode ...")
   149  		err := CmdBg96ForceLTE(m.atCmdPort)
   150  		if err != nil {
   151  			return ret, fmt.Errorf("Error setting scan mode: %v", err.Error())
   152  		}
   153  	}
   154  
   155  	err = CmdFunMin(m.atCmdPort)
   156  	if err != nil {
   157  		return ret, fmt.Errorf("Error setting fun Min: %v", err.Error())
   158  	}
   159  
   160  	err = CmdOK(m.atCmdPort, "AT+QCFG=\"gpio\",1,26,1,0,0,1")
   161  	if err != nil {
   162  		return ret, fmt.Errorf("Error setting GPIO: %v", err.Error())
   163  	}
   164  
   165  	// VZ and Kajeet can use internal VZ SIM, Hologram needs external SIM
   166  	if m.config.APN == APNVerizon || m.config.APN == APNKajeet {
   167  		err = CmdOK(m.atCmdPort, "AT+QCFG=\"gpio\",3,26,1,1")
   168  		if err != nil {
   169  			return ret, fmt.Errorf("Error setting GPIO: %v", err.Error())
   170  		}
   171  
   172  	} else {
   173  		err = CmdOK(m.atCmdPort, "AT+QCFG=\"gpio\",3,26,0,1")
   174  		if err != nil {
   175  			return ret, fmt.Errorf("Error setting GPIO: %v", err.Error())
   176  		}
   177  
   178  	}
   179  
   180  	err = CmdFunFull(m.atCmdPort)
   181  	if err != nil {
   182  		return ret, fmt.Errorf("Error setting fun full: %v", err.Error())
   183  	}
   184  
   185  	// enable GPS. Don't return error of GPS commands fail as
   186  	// this is not a critical error
   187  	err = CmdOK(m.atCmdPort, "AT+QGPS=1")
   188  	if err != nil {
   189  		log.Printf("Error enabling GPS: %v", err.Error())
   190  	}
   191  
   192  	err = CmdOK(m.atCmdPort, "AT+QGPSCFG=\"nmeasrc\",1")
   193  	if err != nil {
   194  		log.Printf("Error settings GPS source: %v", err.Error())
   195  	}
   196  
   197  	sim, err := CmdGetSimBg96(m.atCmdPort)
   198  
   199  	if err != nil {
   200  		return ret, fmt.Errorf("Error getting SIM #: %v", err.Error())
   201  	}
   202  
   203  	ret.Sim = sim
   204  
   205  	imei, err := CmdGetImei(m.atCmdPort)
   206  
   207  	if err != nil {
   208  		return ret, fmt.Errorf("Error getting IMEI #: %v", err.Error())
   209  	}
   210  
   211  	ret.Imei = imei
   212  
   213  	version, err := CmdGetFwVersionBG96(m.atCmdPort)
   214  
   215  	if err != nil {
   216  		return ret, fmt.Errorf("Error getting fw version #: %v", err.Error())
   217  	}
   218  
   219  	ret.Version = version
   220  
   221  	return ret, nil
   222  }
   223  
   224  // Connect stub
   225  func (m *Modem) Connect() error {
   226  	if !m.enabled {
   227  		return errors.New("Connect error, modem disabled")
   228  	}
   229  
   230  	if err := m.openCmdPort(); err != nil {
   231  		return err
   232  	}
   233  
   234  	mode, err := CmdBg96GetScanMode(m.atCmdPort)
   235  
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	log.Println("BG96 scan mode:", mode)
   241  
   242  	if mode != BG96ScanModeLTE {
   243  		log.Println("Setting BG96 scan mode")
   244  		err := CmdBg96ForceLTE(m.atCmdPort)
   245  		if err != nil {
   246  			return err
   247  		}
   248  	}
   249  
   250  	/*
   251  		service, _, _, _, err := CmdQcsq(m.atCmdPort)
   252  		if err != nil {
   253  			return err
   254  		}
   255  
   256  		// TODO need to set APN, etc before we do this
   257  		// but eventually want to make sure we have service
   258  		// before running PPP
   259  		if !service {
   260  
   261  		}
   262  	*/
   263  
   264  	if time.Since(m.lastPPPRun) < 30*time.Second {
   265  		return errors.New("only run PPP once every 30s")
   266  	}
   267  
   268  	m.lastPPPRun = time.Now()
   269  
   270  	log.Println("Modem: starting PPP")
   271  	return exec.Command("pon", m.config.ChatScript).Run()
   272  }
   273  
   274  // GetStatus return interface status
   275  func (m *Modem) GetStatus() (InterfaceStatus, error) {
   276  	if !m.detected() || !m.enabled {
   277  		return InterfaceStatus{}, nil
   278  	}
   279  
   280  	if err := m.openCmdPort(); err != nil {
   281  		return InterfaceStatus{}, err
   282  	}
   283  
   284  	var retError error
   285  	ip, _ := GetIP(m.iface)
   286  
   287  	service, rssi, rsrp, rsrq, err := CmdQcsq(m.atCmdPort)
   288  	if err != nil {
   289  		retError = err
   290  	}
   291  
   292  	var network string
   293  
   294  	if service {
   295  		network, err = CmdCops(m.atCmdPort)
   296  		if err != nil {
   297  			retError = err
   298  		}
   299  	}
   300  
   301  	return InterfaceStatus{
   302  		Detected:  m.detected(),
   303  		Connected: m.pppActive() && service,
   304  		Operator:  network,
   305  		IP:        ip,
   306  		Signal:    rssi,
   307  		Rsrp:      rsrp,
   308  		Rsrq:      rsrq,
   309  	}, retError
   310  }
   311  
   312  // Reset stub
   313  func (m *Modem) Reset() error {
   314  	if m.atCmdPort != nil {
   315  		m.atCmdPort.Close()
   316  		m.atCmdPort = nil
   317  	}
   318  
   319  	err := exec.Command("poff").Run()
   320  	if err != nil {
   321  		log.Println("poff exec error:", err)
   322  	}
   323  	if m.enabled {
   324  		err := m.config.Reset()
   325  		if err != nil {
   326  			return err
   327  		}
   328  	}
   329  
   330  	return nil
   331  }
   332  
   333  // Enable or disable interface
   334  func (m *Modem) Enable(en bool) error {
   335  	log.Println("Modem enable:", en)
   336  	var err error
   337  	m.enabled = en
   338  	if err = m.openCmdPort(); err != nil {
   339  		return err
   340  	}
   341  
   342  	if en {
   343  		err = CmdFunFull(m.atCmdPort)
   344  		if err != nil {
   345  			return err
   346  		}
   347  	} else {
   348  		err = CmdFunMin(m.atCmdPort)
   349  		if err != nil {
   350  			return err
   351  		}
   352  	}
   353  
   354  	return nil
   355  }
   356  
   357  // ErrorModemNotDetected is returned if we try an operation and the modem
   358  // is not detected
   359  var ErrorModemNotDetected = errors.New("No modem detected")
   360  
   361  // GetLocation returns current GPS location
   362  func (m *Modem) GetLocation() (data.GpsPos, error) {
   363  	if !m.detected() {
   364  		return data.GpsPos{}, ErrorModemNotDetected
   365  	}
   366  
   367  	if err := m.openCmdPort(); err != nil {
   368  		return data.GpsPos{}, err
   369  	}
   370  
   371  	line, err := CmdGGA(m.atCmdPort)
   372  
   373  	if err != nil {
   374  		return data.GpsPos{}, err
   375  	}
   376  
   377  	s, err := nmea.Parse(strings.TrimSpace(line))
   378  	if err != nil {
   379  		return data.GpsPos{}, err
   380  	}
   381  
   382  	if s.DataType() != nmea.TypeGGA {
   383  		return data.GpsPos{}, errors.New("GPS not GGA response")
   384  	}
   385  
   386  	gga := s.(nmea.GGA)
   387  	ret := data.GpsPos{}
   388  	ret.FromGPGGA(gga)
   389  	return ret, nil
   390  }