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 }