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 }