github.com/simpleiot/simpleiot@v0.18.3/network/at-commands.go (about)

     1  package network
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"log"
     8  	"regexp"
     9  	"strconv"
    10  	"strings"
    11  )
    12  
    13  // DebugAtCommands can be set to true to
    14  // debug at commands
    15  var DebugAtCommands = false
    16  
    17  // RsrqValueToDb converts AT value to dB
    18  func RsrqValueToDb(value int) float64 {
    19  	if value < 0 || value > 34 {
    20  		// unknown value
    21  		return 0
    22  	}
    23  
    24  	return -20 + float64(value)*0.5
    25  }
    26  
    27  // RsrpValueToDb converts AT value to dB
    28  func RsrpValueToDb(value int) float64 {
    29  	if value < 0 || value > 97 {
    30  		// unknown value
    31  		return 0
    32  	}
    33  
    34  	return -141 + float64(value)
    35  }
    36  
    37  // Cmd send a command to modem and read response
    38  // retry 3 times. Port should be a RespReadWriter.
    39  func Cmd(port io.ReadWriter, cmd string) (string, error) {
    40  	var err error
    41  
    42  	for try := 0; try < 3; try++ {
    43  		if DebugAtCommands {
    44  			log.Println("Modem Tx:", cmd)
    45  		}
    46  
    47  		readString := make([]byte, 512)
    48  
    49  		_, err = port.Write([]byte(cmd + "\r"))
    50  		if err != nil {
    51  			if DebugAtCommands {
    52  				log.Println("Modem cmd write error:", err)
    53  			}
    54  			continue
    55  		}
    56  
    57  		var n int
    58  		n, err = port.Read(readString)
    59  
    60  		if err != nil {
    61  			if DebugAtCommands {
    62  				log.Println("Modem cmd read error:", err)
    63  			}
    64  			continue
    65  		}
    66  
    67  		readString = readString[:n]
    68  
    69  		readStringS := strings.TrimSpace(string(readString))
    70  
    71  		if DebugAtCommands {
    72  			log.Println("Modem Rx:", readStringS)
    73  		}
    74  
    75  		return readStringS, nil
    76  	}
    77  
    78  	return "", err
    79  }
    80  
    81  // CmdOK runs the command and checks for OK response
    82  func CmdOK(port io.ReadWriter, cmd string) error {
    83  	resp, err := Cmd(port, cmd)
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	return checkRespOK(resp)
    89  }
    90  
    91  var errorNoOK = errors.New("command did not return OK")
    92  
    93  func checkRespOK(resp string) error {
    94  	for _, line := range strings.Split(string(resp), "\n") {
    95  		if strings.Contains(line, "OK") {
    96  			return nil
    97  		}
    98  	}
    99  
   100  	return errorNoOK
   101  }
   102  
   103  // +CESQ: 99,99,255,255,13,34
   104  // +CESQ: 99,99,255,255,17,42
   105  // +CESQ: rxlen,ber,rscp,ecno,rsrq,rsrp
   106  var reCesq = regexp.MustCompile(`\+CESQ:\s*(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)`)
   107  
   108  // CmdCesq is used to send the AT+CESQ command
   109  func CmdCesq(port io.ReadWriter) (rsrq, rsrp int, err error) {
   110  	var resp string
   111  	resp, err = Cmd(port, "AT+CESQ")
   112  	if err != nil {
   113  		return
   114  	}
   115  
   116  	found := false
   117  
   118  	for _, line := range strings.Split(string(resp), "\n") {
   119  		matches := reCesq.FindStringSubmatch(line)
   120  
   121  		if len(matches) >= 6 {
   122  			rsrqI, _ := strconv.Atoi(matches[5])
   123  			rsrpI, _ := strconv.Atoi(matches[6])
   124  
   125  			rsrq = int(RsrqValueToDb(rsrqI))
   126  			rsrp = int(RsrpValueToDb(rsrpI))
   127  
   128  			return
   129  		}
   130  	}
   131  
   132  	if !found {
   133  		err = fmt.Errorf("Error parsing CESQ response: %v", resp)
   134  	}
   135  
   136  	return
   137  }
   138  
   139  // service, rssi, rsrp, sinr, rsrq
   140  // +QCSQ: "NOSERVICE"
   141  // +QCSQ: "GSM",-69
   142  // +QCSQ: "CAT-M1",-52,-81,195,-10
   143  var reQcsq = regexp.MustCompile(`\+QCSQ:\s*"(.+)"`)
   144  var reQcsqM1 = regexp.MustCompile(`\+QCSQ:\s*"(.+)",(-*\d+),(-*\d+),(\d+),(-*\d+)`)
   145  
   146  // CmdQcsq is used to send the AT+QCSQ command
   147  func CmdQcsq(port io.ReadWriter) (service bool, rssi, rsrp, rsrq int, err error) {
   148  	var resp string
   149  	resp, err = Cmd(port, "AT+QCSQ")
   150  	if err != nil {
   151  		return
   152  	}
   153  
   154  	found := false
   155  
   156  	for _, line := range strings.Split(string(resp), "\n") {
   157  		matches := reQcsq.FindStringSubmatch(line)
   158  
   159  		if len(matches) < 2 {
   160  			continue
   161  		}
   162  
   163  		found = true
   164  
   165  		serviceS := matches[1]
   166  
   167  		matches = reQcsqM1.FindStringSubmatch(line)
   168  
   169  		if len(matches) >= 6 {
   170  			rssi, _ = strconv.Atoi(matches[2])
   171  			rsrp, _ = strconv.Atoi(matches[3])
   172  			rsrq, _ = strconv.Atoi(matches[5])
   173  		}
   174  
   175  		service = serviceS == "CAT-M1"
   176  	}
   177  
   178  	if !found {
   179  		err = fmt.Errorf("Error parsing QCSQ response: %v", resp)
   180  	}
   181  
   182  	return
   183  }
   184  
   185  // possible return values
   186  // service, rssi, rsrp, sinr, rsrq
   187  // ERROR (if no connection)
   188  // +QSPN: "CHN-UNICOM","UNICOM","",0,"46001"
   189  // +QSPN: "Verizon Wireless","VzW","Hologram",0,"311480"
   190  var reQspn = regexp.MustCompile(`\+QSPN:\s*"(.*)"`)
   191  
   192  // CmdQspn is used to send the AT+QSPN command
   193  func CmdQspn(port io.ReadWriter) (network string, err error) {
   194  	var resp string
   195  	resp, err = Cmd(port, "AT+QSPN")
   196  	if err != nil {
   197  		return
   198  	}
   199  
   200  	found := false
   201  
   202  	for _, line := range strings.Split(string(resp), "\n") {
   203  		matches := reQspn.FindStringSubmatch(line)
   204  
   205  		if len(matches) < 2 {
   206  			continue
   207  		}
   208  
   209  		found = true
   210  
   211  		network = matches[1]
   212  	}
   213  
   214  	if !found {
   215  		err = fmt.Errorf("Error parsing QSPN response: %v", resp)
   216  	}
   217  
   218  	return
   219  }
   220  
   221  // CmdSetApn is used to set the APN using the GCDCONT command
   222  func CmdSetApn(port io.ReadWriter, apn string) error {
   223  	return CmdOK(port, "AT+CGDCONT=3,\"IPV4V6\",\""+apn+"\"")
   224  }
   225  
   226  // CmdFunMin sets the modem functionality to min
   227  func CmdFunMin(port io.ReadWriter) error {
   228  	return CmdOK(port, "AT+CFUN=0")
   229  }
   230  
   231  // CmdFunFull sets the modem functionality to full
   232  func CmdFunFull(port io.ReadWriter) error {
   233  	return CmdOK(port, "AT+CFUN=1")
   234  }
   235  
   236  // CmdAttach attaches modem to network
   237  func CmdAttach(port io.ReadWriter) error {
   238  	return CmdOK(port, "AT+CGATT=1")
   239  }
   240  
   241  // CmdSica is used to send SICA command
   242  func CmdSica(port io.ReadWriter) error {
   243  	return CmdOK(port, "AT^SICA=1,3")
   244  }
   245  
   246  // CmdAt just executes a generic at command
   247  func CmdAt(port io.ReadWriter) error {
   248  	return CmdOK(port, "AT")
   249  }
   250  
   251  // BG96MAR02A07M1G_01.007.01.007
   252  var reCmdVersionBg96 = regexp.MustCompile(`BG96.*`)
   253  
   254  // CmdGetFwVersionBG96 gets FW version from BG96 modem
   255  func CmdGetFwVersionBG96(port io.ReadWriter) (string, error) {
   256  	resp, err := Cmd(port, "AT+CGMR")
   257  	if err != nil {
   258  		return "", err
   259  	}
   260  
   261  	for _, line := range strings.Split(resp, "\n") {
   262  		match := reCmdVersionBg96.FindString(line)
   263  		if match != "" {
   264  			return match, nil
   265  		}
   266  	}
   267  
   268  	return "", fmt.Errorf("Error parsing AT+CGMR response: %v", resp)
   269  }
   270  
   271  // REVISION 4.3.1.0c
   272  var reATI = regexp.MustCompile(`REVISION (\S+)`)
   273  
   274  // CmdATI gets version # from modem
   275  func CmdATI(port io.ReadWriter) (string, error) {
   276  	resp, err := Cmd(port, "ATI")
   277  	if err != nil {
   278  		return "", err
   279  	}
   280  
   281  	for _, line := range strings.Split(resp, "\n") {
   282  		matches := reATI.FindStringSubmatch(line)
   283  		if len(matches) >= 2 {
   284  			return matches[1], nil
   285  		}
   286  	}
   287  
   288  	return "", fmt.Errorf("Error parsing ATI response: %v", resp)
   289  }
   290  
   291  var reUsbCfg = regexp.MustCompile(`USBCFG:\s*(\d+)`)
   292  
   293  // CmdGetUsbCfg gets the USB config. For Telit modems, 0 is ppp, 3 is USB network
   294  func CmdGetUsbCfg(port io.ReadWriter) (int, error) {
   295  	resp, err := Cmd(port, "AT#USBCFG?")
   296  	if err != nil {
   297  		return -1, err
   298  	}
   299  
   300  	for _, line := range strings.Split(string(resp), "\n") {
   301  		matches := reUsbCfg.FindStringSubmatch(line)
   302  		if len(matches) >= 2 {
   303  			cfg, _ := strconv.Atoi(matches[1])
   304  
   305  			return cfg, nil
   306  		}
   307  	}
   308  
   309  	return -1, fmt.Errorf("Error parsing response of USBCFG")
   310  }
   311  
   312  // CmdSetUsbConfig is configures the USB mode of Telit modems
   313  // 0 = ppp
   314  // 3 = usb network
   315  func CmdSetUsbConfig(port io.ReadWriter, cfg int) error {
   316  	return CmdOK(port, fmt.Sprintf("AT#USBCFG=%v", cfg))
   317  }
   318  
   319  var reApn = regexp.MustCompile(`CGDCONT: 3,"IPV4V6","(.*?)"`)
   320  
   321  // CmdGetApn gets the APN
   322  func CmdGetApn(port io.ReadWriter) (string, error) {
   323  	resp, err := Cmd(port, "AT+CGDCONT?")
   324  	if err != nil {
   325  		return "", err
   326  	}
   327  
   328  	for _, line := range strings.Split(resp, "\n") {
   329  		matches := reApn.FindStringSubmatch(line)
   330  		if len(matches) >= 2 {
   331  			apn := matches[1]
   332  			return apn, nil
   333  		}
   334  	}
   335  
   336  	return "", fmt.Errorf("Error parsing AT+CGDCONT?: %v", resp)
   337  }
   338  
   339  var reFwSwitch = regexp.MustCompile(`FWSWITCH:\s*(\d)`)
   340  
   341  // CmdGetFwSwitch returns the firmware used in Telit modems
   342  // 0 - AT&T
   343  // 1 - Verizon
   344  func CmdGetFwSwitch(port io.ReadWriter) (int, error) {
   345  	resp, err := Cmd(port, "AT#FWSWITCH?")
   346  	if err != nil {
   347  		return -1, err
   348  	}
   349  
   350  	for _, line := range strings.Split(resp, "\n") {
   351  		matches := reFwSwitch.FindStringSubmatch(line)
   352  		if len(matches) >= 2 {
   353  			fw, _ := strconv.Atoi(matches[1])
   354  			return fw, nil
   355  		}
   356  	}
   357  
   358  	return -1, fmt.Errorf("Error parsing AT#FWSWITCH?: %v", resp)
   359  }
   360  
   361  // GpioDir specifies Gpio Direction
   362  type GpioDir int
   363  
   364  // Gpio dir values
   365  const (
   366  	GpioDirUnknown GpioDir = -1
   367  	GpioIn         GpioDir = 0
   368  	GpioOut        GpioDir = 1
   369  )
   370  
   371  // GpioLevel describes GPIO level
   372  type GpioLevel int
   373  
   374  // Gpio Level values
   375  const (
   376  	GpioLevelUnknown GpioLevel = -1
   377  	GpioLow          GpioLevel = 0
   378  	GpioHigh         GpioLevel = 1
   379  )
   380  
   381  // #GPIO: 0,0,4
   382  var reGpio = regexp.MustCompile(`GPIO:\s*(\d+),(\d+)`)
   383  
   384  // CmdGetGpio is used to get GPIO state on Telit modems
   385  func CmdGetGpio(port io.ReadWriter, gpio int) (GpioDir, GpioLevel, error) {
   386  	cmd := fmt.Sprintf("AT#GPIO=%v,2", gpio)
   387  	resp, err := Cmd(port, cmd)
   388  	if err != nil {
   389  		return GpioDirUnknown, GpioLevelUnknown, err
   390  	}
   391  
   392  	for _, line := range strings.Split(resp, "\n") {
   393  		matches := reGpio.FindStringSubmatch(line)
   394  		if len(matches) >= 3 {
   395  			dir, _ := strconv.Atoi(matches[1])
   396  			level, _ := strconv.Atoi(matches[2])
   397  			return GpioDir(dir), GpioLevel(level), nil
   398  		}
   399  	}
   400  
   401  	return GpioDirUnknown, GpioLevelUnknown, fmt.Errorf("Error parsing AT#GPIO: %v", resp)
   402  }
   403  
   404  // CmdSetGpio is used to set GPIO state in Telit modems
   405  func CmdSetGpio(port io.ReadWriter, gpio int, level GpioLevel) error {
   406  	err := CmdOK(port, "AT+CFUN=4")
   407  	if err != nil {
   408  		return err
   409  	}
   410  	cmd := fmt.Sprintf("AT#GPIO=%v,%v,1,1", gpio, level)
   411  	err = CmdOK(port, cmd)
   412  	if err != nil {
   413  		return err
   414  	}
   415  	return CmdOK(port, "AT+CFUN=1")
   416  }
   417  
   418  // looking for: +CSQ: 9,99
   419  var reSig = regexp.MustCompile(`\+CSQ:\s*(\d+),(\d+)`)
   420  
   421  // CmdGetSignal gets signal strength
   422  func CmdGetSignal(port io.ReadWriter) (int, int, error) {
   423  	resp, err := Cmd(port, "AT+CSQ")
   424  	if err != nil {
   425  		return 0, 0, err
   426  	}
   427  
   428  	for _, line := range strings.Split(resp, "\n") {
   429  		matches := reSig.FindStringSubmatch(line)
   430  		if len(matches) >= 3 {
   431  			signalStrengthF, _ := strconv.ParseFloat(matches[1], 32)
   432  			bitErrorRateF, _ := strconv.ParseFloat(matches[2], 32)
   433  
   434  			var signalStrength, bitErrorRate int
   435  
   436  			// normalize numbers and return -1 if not known
   437  			if signalStrengthF == 99 {
   438  				signalStrength = -1
   439  			} else {
   440  				signalStrength = int(signalStrengthF * 100 / 31)
   441  			}
   442  
   443  			if bitErrorRateF == 99 {
   444  				bitErrorRate = -1
   445  			} else {
   446  				bitErrorRate = int(bitErrorRateF * 100 / 7)
   447  			}
   448  
   449  			return signalStrength, bitErrorRate, nil
   450  		}
   451  	}
   452  
   453  	return 0, 0, fmt.Errorf("Error parsing AT+CSQ response: %v", resp)
   454  }
   455  
   456  // +CNUM: "Line 1","+15717759540",145
   457  // +CNUM: "","18167882915",129
   458  var reCmdPhoneNum = regexp.MustCompile(`(\d{11,})`)
   459  
   460  // CmdGetPhoneNum gets phone number from modem
   461  func CmdGetPhoneNum(port io.ReadWriter) (string, error) {
   462  	resp, err := Cmd(port, "AT+CNUM")
   463  	if err != nil {
   464  		return "", err
   465  	}
   466  
   467  	for _, line := range strings.Split(resp, "\n") {
   468  		matches := reCmdPhoneNum.FindStringSubmatch(line)
   469  		if len(matches) >= 2 {
   470  			return matches[1], nil
   471  		}
   472  	}
   473  
   474  	return "", fmt.Errorf("Error parsing AT+CNUM response: %v", resp)
   475  }
   476  
   477  // +CCID: "89148000000637720260",""
   478  // +ICCID: 8901260881206806423
   479  var reCmdSim = regexp.MustCompile(`(\d{19,})`)
   480  
   481  // CmdGetSim gets SIM # from modem
   482  func CmdGetSim(port io.ReadWriter) (string, error) {
   483  	resp, err := Cmd(port, "AT+CCID?")
   484  	if err != nil {
   485  		return "", err
   486  	}
   487  
   488  	for _, line := range strings.Split(resp, "\n") {
   489  		matches := reCmdSim.FindStringSubmatch(line)
   490  		if len(matches) >= 2 {
   491  			return matches[1], nil
   492  		}
   493  	}
   494  
   495  	return "", fmt.Errorf("Error parsing AT+CCID? response: %v", resp)
   496  }
   497  
   498  // 356278070013083
   499  var reCmdImei = regexp.MustCompile(`(\d{15,})`)
   500  
   501  // CmdGetImei gets IMEI # from modem
   502  func CmdGetImei(port io.ReadWriter) (string, error) {
   503  	resp, err := Cmd(port, "AT+CGSN")
   504  	if err != nil {
   505  		return "", err
   506  	}
   507  
   508  	for _, line := range strings.Split(resp, "\n") {
   509  		matches := reCmdImei.FindStringSubmatch(line)
   510  		if len(matches) >= 2 {
   511  			return matches[1], nil
   512  		}
   513  	}
   514  
   515  	return "", fmt.Errorf("Error parsing AT+CGSN response: %v", resp)
   516  }
   517  
   518  // CmdGetSimBg96 returns SIM for bg96 modems
   519  func CmdGetSimBg96(port io.ReadWriter) (string, error) {
   520  	resp, err := Cmd(port, "AT+QCCID")
   521  	if err != nil {
   522  		return "", err
   523  	}
   524  
   525  	for _, line := range strings.Split(resp, "\n") {
   526  		matches := reCmdSim.FindStringSubmatch(line)
   527  		if len(matches) >= 2 {
   528  			return matches[1], nil
   529  		}
   530  	}
   531  
   532  	return "", fmt.Errorf("Error parsing AT+QCCID response: %v", resp)
   533  }
   534  
   535  // +QGPSGNMEA: $GPGGA,,,,,,0,,,,,,,,*66
   536  var reQGPSNEMA = regexp.MustCompile(`\+QGPSGNMEA:\s*(.*)`)
   537  
   538  // CmdGGA gets GPS information from modem
   539  func CmdGGA(port io.ReadWriter) (string, error) {
   540  	resp, err := Cmd(port, "AT+QGPSGNMEA=\"GGA\"")
   541  	if err != nil {
   542  		return "", err
   543  	}
   544  
   545  	for _, line := range strings.Split(resp, "\n") {
   546  		matches := reQGPSNEMA.FindStringSubmatch(line)
   547  		if len(matches) >= 2 {
   548  			return matches[1], nil
   549  		}
   550  	}
   551  
   552  	return "", fmt.Errorf("Error parsing AT+QGPSGNMEA response: %v", resp)
   553  }
   554  
   555  // CmdBg96ForceLTE forces BG96 modems to use LTE only, (no 2G)
   556  func CmdBg96ForceLTE(port io.ReadWriter) error {
   557  	return CmdOK(port, "AT+QCFG=\"nwscanmode\",3,1")
   558  }
   559  
   560  // BG96ScanMode is a type that defines the varios BG96 scan modes
   561  type BG96ScanMode int
   562  
   563  // valid scan modes
   564  const (
   565  	BG96ScanModeUnknown BG96ScanMode = -1
   566  	BG96ScanModeAuto    BG96ScanMode = 0
   567  	BG96ScanModeGSM     BG96ScanMode = 1
   568  	BG96ScanModeLTE     BG96ScanMode = 3
   569  )
   570  
   571  // +QCFG: "nwscanmode",3
   572  var reBg96ScanMode = regexp.MustCompile(`\++QCFG: "nwscanmode",(\d)`)
   573  
   574  // CmdBg96GetScanMode returns the current modem scan mode
   575  func CmdBg96GetScanMode(port io.ReadWriter) (BG96ScanMode, error) {
   576  	resp, err := Cmd(port, "AT+QCFG=\"nwscanmode\"")
   577  	if err != nil {
   578  		return BG96ScanModeUnknown, err
   579  	}
   580  
   581  	for _, line := range strings.Split(resp, "\n") {
   582  		matches := reBg96ScanMode.FindStringSubmatch(line)
   583  		if len(matches) >= 2 {
   584  			mode, err := strconv.Atoi(matches[1])
   585  			if err != nil {
   586  				continue
   587  			}
   588  
   589  			return BG96ScanMode(mode), nil
   590  		}
   591  	}
   592  
   593  	return BG96ScanModeUnknown,
   594  		fmt.Errorf("Error parsing AT+QGPSGNMEA response: %v", resp)
   595  }
   596  
   597  // TODO, add AT+COPS command to get current carrier
   598  // AT+COPS?
   599  // +COPS: 0 (no connection)
   600  // +COPS: 0,0,"AT&T Hologram",8
   601  var reCops = regexp.MustCompile(`\+COPS:`)
   602  var reCopsCon = regexp.MustCompile(`\+COPS:\s*(.*),(.*),"(.+)"`)
   603  
   604  // CmdCops is used determine what carrier we are connected to
   605  func CmdCops(port io.ReadWriter) (carrier string, err error) {
   606  	var resp string
   607  	resp, err = Cmd(port, "AT+COPS?")
   608  	if err != nil {
   609  		return
   610  	}
   611  
   612  	found := false
   613  
   614  	for _, line := range strings.Split(string(resp), "\n") {
   615  		if reCops.FindStringIndex(line) == nil {
   616  			continue
   617  		}
   618  
   619  		found = true
   620  
   621  		matches := reCopsCon.FindStringSubmatch(line)
   622  
   623  		if len(matches) >= 4 {
   624  			carrier = matches[3]
   625  		}
   626  	}
   627  
   628  	if !found {
   629  		err = fmt.Errorf("Error parsing COPS? response: %v", resp)
   630  	}
   631  
   632  	return
   633  }
   634  
   635  // CmdReboot reboots modem
   636  func CmdReboot(port io.ReadWriter) error {
   637  	return CmdOK(port, "AT+CFUN=1,1")
   638  }