├── .gitignore ├── LICENSE ├── README.md ├── build.sh ├── images └── release.png ├── project.properties └── src ├── CellularHelp.cpp ├── CellularHelp.h ├── CellularInterpreterRK.cpp ├── CellularInterpreterRK.h ├── CloudDebug.cpp ├── CloudDebug.h ├── CloudDebugCellular.cpp ├── CloudDebugEthernet.cpp ├── CloudDebugWiFi.cpp ├── Tinker.cpp └── sourcever.h /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | *.bin 3 | lib 4 | .DS_Store 5 | target 6 | release 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Particle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloud Debug - Tool for debugging cellular, Wi-Fi and cloud connectivity issues 2 | 3 | You should use the [web device doctor](https://docs.particle.io/tools/doctor/) instead of using cloud debug. The web device doctor 4 | now contains all of the features of cloud debug and also can debug cloud-side configuration and activation issues. 5 | 6 | The firmware in this repository is no longer maintained and new version will not be updated in the future. 7 | 8 | --- 9 | 10 | ## Installing 11 | 12 | ### Using the Chrome browser 13 | 14 | If you are using the Chrome web browser on Windows 10, Mac, Linux, Chromebook, or some Android phones, you can install cloud debug right from the web page with no additional software to download or install. 15 | 16 | [Web-based Cloud Debug Install](https://docs.particle.io/cloud-debug/) 17 | 18 | This feature is not available on other browsers or iOS devices (iPhone and iPad). 19 | 20 | ### Install the Particle CLI 21 | 22 | If you haven't already installed the [Particle CLI](https://docs.particle.io/tutorials/developer-tools/cli/), so should do so now. 23 | 24 | Once installed, you will enter Particle CLI commands in a Command Prompt or Terminal window. 25 | 26 | ### Update Device OS 27 | 28 | The cloud debug binaries target a variety of versions of Device OS (see release notes, below). 29 | 30 | If you have an older version of Device OS installed you will want to manually upgrade it by USB. While devices can update Device OS OTA (over-the-air), presumably if you need the cloud debugging tool you're having trouble connecting to the cloud, and thus will not be able to upgrade OTA. 31 | 32 | The easiest method is to use the web-based installer, above. However you can often use the Particle CLI: 33 | 34 | - Connect the device by USB to your computer. 35 | - Enter DFU mode (blinking yellow) by holding down MODE button and tapping RESET. Continue to hold down MODE while the status LED blinks yellow until it blinks magenta (red and blue at the same time) and release. 36 | - Enter the following command in a Terminal or Command Prompt window: 37 | 38 | ``` 39 | particle update 40 | ``` 41 | 42 | The exception is the P2. The `particle update` command does not update to Device OS 5.0.1 needed by cloud debug. 43 | 44 | ### Download the binary 45 | 46 | The binaries are included in Github as releases. The latest release is on Github on the right-hand size of the window under **Releases** 47 | 48 | ![Release](images/release.png) 49 | 50 | The releases match the major and minor version of Device OS that's targeted. For example, 1.5.1 targets 1.5.x, in this case 1.5.2. As other versions of cloud-debug are released, those will have increasing patch versions, like 1.5.1, 1.5.2, etc.. The last digit will continue to increase for each new version of the cloud-debug code and does not exactly match the Device OS version! 51 | 52 | Because binaries are generally compatible with later versions of Device OS, you don't need to have an exact match on the Device OS versions and you do not need to downgrade your Device OS binary. 53 | 54 | Since user firmware binaries are backward compatible, the 1.5.1 version will work on 2.0.x and 3.0.x, but if we need to take advantage of features only included in a later version we may include multiple binary targets in the future. 55 | 56 | There is a pre-built binary for each device that is supported, such as: 57 | 58 | | Gen | Network | Name | Details | 59 | | :---: | :------- | :--- | :--- | 60 | | Gen 2 | Wi-Fi | photon.bin | | 61 | | Gen 2 | Wi-Fi | p1.bin | | 62 | | Gen 2 | Cellular | electron.bin | Also E Series, all SKUs | 63 | | Gen 3 | Wi-Fi | argon.bin | | 64 | | Gen 3 | Wi-Fi | p2.bin | | 65 | | Gen 3 | Cellular | boron.bin | Both Boron 2G/3G (BRN310, BRN314) and Boron LTE (BRN402, BRN404, BRN404X) | 66 | | Gen 3 | Cellular | bsom.bin | B Series LTE Cat M1 (North America) (B402, B404, B404X) | 67 | | Gen 3 | Cellular | b5som.bin | B Series LTE Cat 1 with 2G/3G (EMEAAA) (B523, B524) | 68 | | Gen 3 | Cellular | tracker.bin | Tracker SoM and Tracker One, T402, T404, T523, T524, and equivalent ONE models | 69 | 70 | ### Flashing the binary to the device 71 | 72 | - Connect the device by USB to your computer. 73 | - Enter DFU mode (blinking yellow): 74 | 75 | ``` 76 | particle usb dfu 77 | ``` 78 | 79 | - Enter the following command in a Terminal or Command Prompt window: 80 | 81 | ``` 82 | cd Downloads 83 | particle flash --usb boron.bin 84 | ``` 85 | 86 | Modify the cd command if you've saved the binary somewhere else. And of course replace boron.bin with the bin file for your device. 87 | 88 | 89 | ## Viewing the logs 90 | 91 | The easiest way to view the logs is to use the Particle CLI: 92 | 93 | ``` 94 | particle serial monitor 95 | ``` 96 | 97 | Note, however, that particle serial monitor is only a monitor. You cannot type commands into the window to control the device. For that, see the section below, Using the command line. 98 | 99 | ### Decoding the logs 100 | 101 | These application notes may help decode the logs. 102 | 103 | - [AN003 Interpreting Cloud Debug](https://docs.particle.io/datasheets/app-notes/an003-interpreting-cloud-debug/) shows how to interpret cloud debugging logs to troubleshoot various common issues. 104 | - [AN004 Interpreting Cloud Debug-2](https://docs.particle.io/datasheets/app-notes/an004-interpreting-cloud-debug-2/) is a deep dive into interpreting cloud debug logs and cross-referencing the AT command guide for the u-blox modem. 105 | 106 | ## Using the cloud debug command line 107 | 108 | The particle serial monitor command is handy for viewing the output from the cloud debug tool. However, you can't type commands to control the tool with it. 109 | 110 | ### Mac and Linux 111 | 112 | On Mac OS (OS X) and Linux systems, you can access the serial port through the terminal. 113 | 114 | For Mac OS, open the terminal and type: 115 | 116 | ``` 117 | screen /dev/tty.u 118 | ``` 119 | 120 | and pressing tab to autocomplete. 121 | 122 | On Linux, you can accomplish the same thing by using: 123 | 124 | ``` 125 | screen /dev/ttyACM 126 | ``` 127 | 128 | and pressing tab to autocomplete. 129 | 130 | Now you are ready to read data sent by the device over Serial and send data back. 131 | 132 | 133 | ### Windows 134 | 135 | For Windows users, we recommend downloading [PuTTY](http://www.putty.org/). Plug your device into your computer over USB, open a serial port in PuTTY using the standard settings, which should be: 136 | 137 | - Baud rate: 9600 138 | - Data Bits: 8 139 | - Parity: none 140 | - Stop Bits: 1 141 | 142 | 143 | ### Cellular commands 144 | 145 | #### cellular 146 | 147 | ``` 148 | > cellular -c 149 | > cellular -d 150 | ``` 151 | 152 | Connect (-c) or disconnect (-d) from cellular. This only will work reliably after you've successfully connected to the cellular. Executing this during a cloud test may fail. 153 | 154 | #### setCredentials 155 | 156 | ``` 157 | > setCredentials -a "broadband" 158 | ``` 159 | 160 | Sets the APN, and optionally the username and password, for a 3rd-party SIM card. 161 | 162 | | Parameter | Details | Required | 163 | | :--- | :--- | :--- | 164 | | -a | APN | Yes | 165 | | -u | Username | No | 166 | | -p | Password | No | 167 | 168 | 169 | Boron: This setting is persistent and will stay in effect through reboots and Device OS upgrades. 170 | 171 | Electron 2G/3G: Supported, however the setting is not persistent and will be reset when the cellular modem is powered down. 172 | 173 | B Series SoM, Tracker SoM, E Series, Electron LTE: Not supported as these devices do not support 3rd-party SIM cards. 174 | 175 | #### clearCredentials 176 | 177 | ``` 178 | > clearCredentials 179 | ``` 180 | 181 | Clears the APN, username, and password for a 3rd-party SIM card. This setting is persistent and will stay in effect through reboots and Device OS upgrades. 182 | 183 | Supported on the Boron only. The B Series SoM and Tracker SoM do not support 3rd-party SIM cards. 184 | 185 | On the Electron and E Series, removing all power (battery and USB) for 10 seconds will reset credentials. 186 | 187 | #### setActiveSim 188 | 189 | ``` 190 | > setActiveSim -i 191 | > setActiveSim -e 192 | ``` 193 | 194 | Sets the active SIM card to internal Particle SIM (-i) or external (-e). This setting is persistent and will stay in effect through reboots and Device OS upgrades. 195 | 196 | Supported on the Boron only. The B Series SoM and Tracker SoM do not support 3rd-party SIM cards. 197 | 198 | Note: On the Boron LTE you must not only set the SIM to internal (-i) but you must physically remove the SIM card from the 4FF nano SIM card slot as well. 199 | 200 | 201 | #### mnoprof 202 | 203 | ``` 204 | > mnoprof 100 205 | ``` 206 | 207 | Sets the Mobile Network Operator Profile number on Boron LTE Cat M1 devices. Setting this incorrectly will prevent the device from connecting to cellular (stuck on blinking green). 208 | 209 | You will need to reset the modem after setting the MNO profile. Sending the command `AT+CFUN=15` is one way. 210 | 211 | Supported on the Boron LTE only. The B Series SoM and Tracker SoM do not support 3rd-party SIM cards. This setting is persistent and will stay in effect through reboots and Device OS upgrades. 212 | 213 | #### command 214 | 215 | ``` 216 | > command AT 217 | > command AT+COPS=3 218 | ``` 219 | 220 | Send a raw AT command to the modem. Everything after "command" is passed directly to Cellular.command, and a `\r\n` is appended to the end. Trace logging is enabled for the duration of the command so you can see the results. 221 | 222 | ### Wi-Fi commands 223 | 224 | #### wifi 225 | 226 | ``` 227 | > wifi -c 228 | > wifi -d 229 | ``` 230 | 231 | Connect (-c) or disconnect (-d) from Wi-Fi. This only will work reliably after you've successfully connected to the Wi-Fi. Executing this during a cloud test may fail. 232 | 233 | #### antenna 234 | 235 | ``` 236 | > antenna -i 237 | > antenna -e 238 | > antenna -a 239 | ``` 240 | 241 | Select the Wi-Fi antenna on the Photon, P0, and P1. This option is persistent and will stay in effect through reboots, Device OS upgrades, and reset credentials. 242 | 243 | | Option | Use | Details | 244 | | :--- | :--- | :--- | 245 | | -i | Internal | Default on Photon and P1 | 246 | | -e | External | External (U.FL connector) | 247 | | -a | Automatic | Available on Photon and P1, not normally enabled | 248 | 249 | The Argon does not have an internal antenna and does not support this option. 250 | 251 | #### clearCredentials 252 | 253 | ``` 254 | > clearCredentials 255 | ``` 256 | 257 | This command clears Wi-Fi credentials. 258 | 259 | Supported on the Photon, P1, and Argon. 260 | 261 | 262 | #### setCredentials 263 | 264 | ``` 265 | > setCredentials -s "My Network" -p "secret" 266 | > setCredentials -s "My Network" -p "secret" -t WPA 267 | ``` 268 | 269 | This command sets Wi-Fi credentials. The setting is persistent and survives reset and Device OS upgrades. 270 | 271 | | Parameter | Details | Required | 272 | | :--- | :--- | :--- | 273 | | -s | SSID | Yes | 274 | | -p | Password | Yes, unless the network is open/unsecured | 275 | | -t | Authentication Type | No, unless the AP is not available | 276 | 277 | The authentication type is inferred when the network is able to be connected to when setCredentials is called. However, if you are pre-configuring authentication credentials for networks not available at the time of the call, you must specify the authentication type (case-sensitive): 278 | 279 | - WEP 280 | - WPA 281 | - WPA2 282 | 283 | Supported on the Photon, P1, and Argon. 284 | 285 | #### useDynamicIP 286 | 287 | ``` 288 | > useDynamicIP 289 | ``` 290 | 291 | Sets dynamic IP address mode. This is the factory default, and resets a static IP set with useStaticIP. 292 | 293 | This option is persistent and will stay in effect through reboots, Device OS upgrades, and reset credentials. 294 | 295 | Supported on the Photon and P1 only. The Argon does not support static IP addresses. 296 | 297 | #### setStaticIP 298 | 299 | ``` 300 | > setStaticIP -a 192.168.1.5 -s 255.255.255.0 -g 192.168.1.1 -d 192.168.1.1 301 | ``` 302 | 303 | Sets static IP address mode. This option is persistent and will stay in effect through reboots, Device OS upgrades, and reset credentials. All four parameters are required: 304 | 305 | | Parameter | Details | Required | 306 | | :--- | :--- | :--- | 307 | | -a | IP address | Yes | 308 | | -s | Subnet mask | Yes | 309 | | -g | Gateway address | Yes | 310 | | -d | DNS server address | Yes | 311 | 312 | Supported on the Photon and P1 only. The Argon does not support static IP addresses. 313 | 314 | ### Common commands 315 | 316 | #### keepAlive 317 | 318 | ``` 319 | > keepAlive 5 320 | ``` 321 | 322 | Set the Particle Cloud keep-alive value in minutes. 323 | 324 | This is available on: 325 | 326 | - Gen 2 cellular devices (Electron, E Series) 327 | - Gen 3 devices (Argon, Boron, B Series SoM, Tracker SoM) 328 | 329 | #### cloud 330 | 331 | ``` 332 | > cloud -c 333 | > cloud -d 334 | ``` 335 | 336 | Connect (-c) or disconnect (-d) from the cloud. This only will work reliably after you've successfully connected to the cloud. Executing this during a cloud test may fail. 337 | 338 | #### trace 339 | 340 | ``` 341 | > trace -e 342 | > trace -d 343 | ``` 344 | 345 | Enable (-e) or disable (-d) trace logging. This affects how verbose the logs are. On the Electron/E Series in particular, the logs get very long in trace mode. 346 | 347 | #### safeMode 348 | 349 | ``` 350 | > safeMode 351 | ``` 352 | 353 | Enter safe mode (breathing magenta). However, if you are having cloud connection problems, this may not work as in order to enter safe mode, you need to be able to connect to the cloud. 354 | 355 | #### dfu 356 | 357 | ``` 358 | > dfu 359 | ``` 360 | 361 | Enter DFU mode (blinking yellow). It may make more sense to use `particle usb dfu` but this command is available. 362 | 363 | 364 | 365 | ## Cellular tower scan 366 | 367 | Some devices with cellular modems can do a tower scan, which shows available GSM cellular carriers. 368 | 369 | | Device | Tower Scan Supported | 370 | | :--- | :---: | 371 | | Electron G350 | ✓ | 372 | | Electron U260 | ✓ | 373 | | Electron U270 | ✓ | 374 | | E Series E310 | ✓ | 375 | | E Series E402 | | 376 | | Boron 2G/3G | ✓ | 377 | | Boron LTE | | 378 | | B Series B402 | | 379 | | B Series B523 | | 380 | | Tracker T402 | | 381 | | Tracker T523 | | 382 | 383 | Of note: 384 | 385 | - This does not work on LTE Cat M1 devices (E402, Boron LTE, B402) or Quectel devices (B523, Tracker SoM) as the hardware does not support it. 386 | - It can only detect towers that support 2G/3G (not LTE or LTE Cat M1). 387 | - It takes a few minutes to scan for towers. 388 | 389 | To use the tower scan, use the `tower` command from the cloud debug command line, or tap the MODE button within the first 10 seconds after booting. 390 | 391 | 392 | ## Removing 393 | 394 | You can remove the software by flashing another application, either your own, or a standard application like Tinker. 395 | 396 | To do it using the CLI: 397 | 398 | - Connect the device by USB to your computer. 399 | - Enter DFU mode (blinking yellow) by holding down MODE button and tapping RESET. Continue to hold dowh MODE while the status LED blinks yellow until it blinks magenta (red and blue at the same time) and release. 400 | - Enter the following command in a Terminal or Command Prompt window: 401 | 402 | ``` 403 | particle flash --usb tinker 404 | ``` 405 | 406 | ## Building from source 407 | 408 | You can also build cloud-debug from source. You may want to do this to customize behavior, such as logging to Serial1 (USART serial) instead of USB serial on a P1 or B Series SoM without USB. 409 | 410 | Note that you cannot target versions of Device OS older than 1.2.1. The code requires features built into that version and will not compile on earlier versions. 411 | 412 | ### In Particle Workbench 413 | 414 | - Clone [this repository](https://github.com/particle-iot/cloud-debug). 415 | - In Particle Workbench use **Particle: Import Project** to load the project. 416 | - Select your device type and Device OS version using **Particle: Configure Project For Device** 417 | - Build (either local or cloud)! 418 | 419 | Note that there are several libraries required. These are automatically loaded from the project.properties file. 420 | 421 | - [CellularHelper](https://github.com/particle-iot/CellularHelper) 422 | - [SerialCommandParserRK](https://github.com/rickkas7/SerialCommandParserRK) 423 | - [CarrierLookupRK](https://github.com/rickkas7/CarrierLookupRK) 424 | 425 | Because of the number of files and libraries, it's not particularly convenient to use the Web IDE; it's much easier to use Workbench or the CLI compiler. 426 | 427 | ### In Particle CLI 428 | 429 | - Clone [this repository](https://github.com/particle-iot/cloud-debug). 430 | - Build it using the Particle CLI (cloud compile): 431 | 432 | ``` 433 | cd cloud-debug 434 | particle compile boron . --target 2.0.0-rc.2 --saveTo boron.bin 435 | ``` 436 | 437 | ## Handy New Features 438 | 439 | If you've used the previous versions of [Electron Cloud Debug](https://github.com/rickkas7/electron-clouddebug), [Photon Cloud Debug](https://github.com/rickkas7/photon-clouddebug), or [Boron Cloud Debug](https://github.com/rickkas7/boron-clouddebug), there are a bunch of new features that make troubleshooting more productive. 440 | 441 | More information about versions is now displayed: 442 | 443 | ``` 444 | 0000015479 [app] INFO: Platform: Boron 445 | 0000015479 [app] INFO: Binary compiled for: 1.5.2 446 | 0000015480 [app] INFO: Cloud Debug Release 1.5.1 447 | 0000015480 [app] INFO: System version: 2.0.0-rc.2 448 | 0000015481 [app] INFO: Device ID: e00fce6ffffffffe7d5cd238 449 | ``` 450 | 451 | Power source and battery information is displayed (when available): 452 | 453 | ``` 454 | 0000015340 [app] INFO: Power source: USB Host 455 | 0000015342 [app] INFO: Battery state: charged, SoC: 77.48 456 | ``` 457 | 458 | On devices with a bq24195 PMIC (Electron, E Series, Boron, Tracker SoM), some common settings are displayed now: 459 | 460 | ``` 461 | 0000023359 [app] INFO: PMIC inputVoltageLimit: 3880 mV 462 | 0000023360 [app] INFO: PMIC inputCurrentLimit: 500 mA 463 | 0000023361 [app] INFO: PMIC minimumSystemVoltage: 3500 mV 464 | 0000023362 [app] INFO: PMIC chargeCurrentValue: 896 mA 465 | 0000023362 [app] INFO: PMIC chargeVoltageValue: 4112 mV 466 | ``` 467 | 468 | Several commands are now decoded and helpful information is shown as [app.help] messages: 469 | 470 | ``` 471 | 0000034916 [ncp.at] TRACE: > AT+UMNOPROF? 472 | 0000034923 [ncp.at] TRACE: < +UMNOPROF: 2 473 | 0000038600 [app.help] INFO: Mobile Network Operator Profile (UMNOPROF): AT&T (2) 474 | ``` 475 | 476 | ``` 477 | 0000026000 [ncp.at] TRACE: > AT+CREG? 478 | 0000026000 [ncp.at] TRACE: < +CREG: 2,5,"2CF7","8AFFF6F",6 479 | 0000026493 [app.help] INFO: Network registration (CREG) Status: registered, roaming (5) 480 | 0000026494 [app.help] INFO: Tracking area code: 2CF7 481 | 0000026494 [app.help] INFO: Cell identifier: 8AFFF6F 482 | 0000026495 [app.help] INFO: Access technology: GSM + HSDPA & USUPA (6) 483 | 0000026000 [ncp.at] TRACE: < OK 484 | ``` 485 | 486 | ``` 487 | 0000031664 [ncp.at] TRACE: > AT+USOCTL=1,0 488 | 0000031672 [ncp.at] TRACE: < +CME ERROR: operation not allowed 489 | 0000031675 [app.help] INFO: operation not allowed is expected here. 490 | ``` 491 | 492 | ``` 493 | 0000038054 [ncp.at] TRACE: > AT+CSQ 494 | 0000038059 [ncp.at] TRACE: < +CSQ: 13,3 495 | 0000038063 [app.help] INFO: Signal Quality (CSQ): rssi=-87 dBm qual=3 (0-7, lower is better) 496 | 0000038061 [ncp.at] TRACE: < OK 497 | ``` 498 | 499 | Operator codes (MCC/MNC) are decoded world-wide. The database is in the cloud debug firmware so it works without cellular and on all modems. 500 | 501 | ``` 502 | 0000044000 [ncp.at] TRACE: > AT+COPS? 503 | 0000044000 [ncp.at] TRACE: < +COPS: 0,2,"310410",2 504 | 0000044254 [app.help] INFO: Operator Selection (COPS) Read: mode=automatic (0) 505 | 0000044255 [app.help] INFO: format=numeric (2) 506 | 0000044257 [app.help] INFO: oper=310410 carrier=AT&T Wireless Inc. country=United States 507 | ``` 508 | 509 | Information about the carrier, signal strength, power source, and battery is periodically displayed for cellular devices: 510 | 511 | ``` 512 | 0000064023 [app] INFO: Cloud connected for 00:30 513 | 0000065817 [app] INFO: Cellular Info: cid=149999050 lac=11511 mcc=310 mnc=410 514 | 0000065818 [app] INFO: Carrier: AT&T Wireless Inc. Country: United States 515 | 0000065848 [app] INFO: Technology: 3G, Band: UMTS 850 516 | 0000065868 [app] INFO: Strength: 36.5, Quality: 75.5, RAT: 3G 517 | 0000065868 [app] INFO: Power source: USB Host 518 | 0000065869 [app] INFO: Battery state: charged, SoC: 99.96 519 | ``` 520 | 521 | It shows the technology, general frequency (700, 850, 900, etc.), and band number for LTE: 522 | 523 | ``` 524 | 0000247585 [app] INFO: Technology: LTE Cat M1, Band: LTE 700 (B12) 525 | ``` 526 | 527 | It keeps track of how long connecting to cellular and connecting to the cloud take: 528 | 529 | ``` 530 | 0000224665 [app] INFO: Still trying to connect to cellular 03:11 531 | ``` 532 | 533 | ``` 534 | 0001557289 [system] INFO: Cloud: disconnected 535 | 0001557290 [app] INFO: Lost cloud connection after 25:27 536 | ``` 537 | 538 | Logging of the modem information is now delayed until the modem is responding to commands: 539 | 540 | ``` 541 | 0000026472 [app] INFO: manufacturer: u-blox 542 | 0000026478 [app] INFO: model: SARA-U260 543 | 0000026484 [app] INFO: firmware version: 23.20 544 | 0000026501 [app] INFO: ordering code: SARA-U260-00S-00 545 | 0000026512 [app] INFO: IMEI: 359999999907929 546 | 0000026524 [app] INFO: ICCID: 8934000000000004028 547 | ``` 548 | 549 | When cloud connected it logs the device name and public IP address (for both cellular and Wi-Fi): 550 | 551 | ``` 552 | 0000041093 [app] INFO: Device name: electron3 553 | 0000041498 [app] INFO: Public IP address: 176.83.141.209 554 | ``` 555 | 556 | It periodically shows signal strength and base station MAC address for Wi-Fi: 557 | 558 | ``` 559 | 0000072193 [app] INFO: Cloud connected for 00:20 560 | 0000072195 [app] INFO: rssi=-72.0 bssid=FC:FF:FF:FF:0C:55 561 | ``` 562 | 563 | It can detect if Ethernet is present, and logs IP address information when present: 564 | 565 | ``` 566 | 0000015342 [app] INFO: This device could have Ethernet (is 3rd generation) 567 | 0000015344 [app] INFO: FEATURE_ETHERNET_DETECTION not enabled, so Ethernet will not be used (even if present) 568 | ``` 569 | 570 | On the Electron and E Series the extraordinarily verbose logs are now tamed and look like the Gen 3 logs with just the essential command and response information. If you really want the old behavior back, the `literal -e` (enable) cloud debug command will turn on the old, completely unfiltered behavior. 571 | 572 | ``` 573 | 42.009 AT send 36 "AT+USOST=1,\"3.210.194.186\",5684,33\r\n" 574 | 42.017 AT read > 3 "\r\n@" 575 | 42.017 AT send 33 "\x17\xfe\xfd\x00\x01\x00\x00\x00\x00\x00\r\x00\x14\x00\x01\x00\x00\x00\x00\x00\r\xb9P\xae\xb2\xa3\x8b\x93|N\a\x94u" 576 | 42.153 AT read + 16 "\r\n+USOST: 1,33\r\n" 577 | 42.154 AT read OK 6 "\r\nOK\r\n" 578 | 42.154 AT send 4 "AT\r\n" 579 | 42.157 AT read OK 6 "\r\nOK\r\n" 580 | 42.157 AT send 14 "AT+USORF=1,0\r\n" 581 | 42.164 AT read + 15 "\r\n+USORF: 1,0\r\n" 582 | Socket 1: handle 1 has 0 bytes pending 583 | 42.167 AT read OK 6 "\r\nOK\r\n" 584 | ``` 585 | 586 | 587 | ## Version History 588 | 589 | ### 4.0.5 (2022-09-23) 590 | 591 | - Report MNO profile 100 as manual band configuration instead of European to reduce confusion 592 | - Targets Device OS 2.3.0 on Electron/E Series, P1, and Photon 593 | - Targets Device OS 4.0.0 on argon, boron, bsom, b5som, tracker 594 | - Targets Device OS 5.0.1 on p2 595 | 596 | ### 2.1.4 (2021-06-16) 597 | 598 | - Fix modem information sometimes not reported on Gen 3 (ch70101) 599 | - Targets device OS 2.1.0 on all platforms 600 | 601 | ### 1.5.3 (2020-10-21) 602 | 603 | - Fixed erroneous help message on read of CREG/CEREG/CGREG; displayed set help, not read help. 604 | - README clarifications 605 | 606 | ### 1.5.2 (2020-10-15) 607 | 608 | - Fixed memory leak on Quectel devices 609 | - Added memory usage reporting 610 | 611 | ### 1.5.1 (2020-10-07) 612 | 613 | - Initial preview/testing version 614 | 615 | 616 | 617 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SOURCEVER=5 4 | 5 | echo "const int SOURCEVER = $SOURCEVER;" > src/sourcever.h 6 | 7 | mkdir release || set status 0 8 | 9 | for PLATFORM in photon p1 electron 10 | do 11 | particle compile $PLATFORM . --target 2.3.0 --saveTo release/$PLATFORM.bin 12 | done 13 | 14 | for PLATFORM in argon boron bsom b5som tracker esomx 15 | do 16 | particle compile $PLATFORM . --target 4.0.0 --saveTo release/$PLATFORM.bin 17 | done 18 | 19 | for PLATFORM in p2 20 | do 21 | particle compile $PLATFORM . --target 5.0.1 --saveTo release/$PLATFORM.bin 22 | done 23 | -------------------------------------------------------------------------------- /images/release.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/particle-iot/cloud-debug/fe99a6aee544797bbc9b1248313d9d45e4ec2527/images/release.png -------------------------------------------------------------------------------- /project.properties: -------------------------------------------------------------------------------- 1 | name=cloud-debug 2 | dependencies.CellularHelper=0.2.5 3 | dependencies.SerialCommandParserRK=0.0.8 4 | dependencies.CarrierLookupRK=0.0.1 5 | -------------------------------------------------------------------------------- /src/CellularHelp.cpp: -------------------------------------------------------------------------------- 1 | #include "CellularHelp.h" 2 | #include "CellularInterpreterRK.h" 3 | #include "CarrierLookupRK.h" 4 | 5 | #include "CloudDebug.h" // for elapsedString(), probably should move 6 | 7 | static Logger _log("app.help"); 8 | 9 | static const char _cregnMapping[] = 10 | "0: default\n" 11 | "1: stat enabled\n" 12 | "2: location enabled\n"; 13 | 14 | 15 | static const char _cregStatMapping[] = // 6, 7, 9, 10 are CREG only 16 | "0: not registered, not searching\n" 17 | "1: registered, home network\n" 18 | "2: not registered, searching\n" 19 | "3: registration denied\n" 20 | "4: unknown, may be out of coverage area\n" 21 | "5: registered, roaming\n" 22 | "6: registered, SMS only, home network\n" // CREG only 23 | "7: registered, SMS only, roaming\n" // CREG only 24 | "8: emergency service only\n" // CEREG only 25 | "9: registered CSFB not preferred, home network\n" // CREG only 26 | "10: registered CSFB not preferred, roaming\n"; // CREG only 27 | 28 | static const char _cregActMapping[] = 29 | "0: GSM\n" // CREG only 30 | "1: GSM COMPACT\n" // CREG only 31 | "2: UTRAN\n" // CREG only 32 | "3: GSM + EDGE\n" // CREG only 33 | "4: GSM + HSDPA\n" // CREG only 34 | "5: GSM + USUPA\n" // CREG only 35 | "6: GSM + HSDPA & USUPA\n" // CREG only 36 | "7: LTE\n" // CEREG only 37 | "8: LTE Cat-M1\n" // CEREG only 38 | "9: LTE Cat-NB1\n"; // CEREG only 39 | 40 | static const char _copsModeMapping[] = 41 | "0: automatic\n" 42 | "1: manual\n" 43 | "2: deregister\n" 44 | "3: set format\n" 45 | "4: manual/automatic\n"; 46 | 47 | static const char _copsFormatMapping[] = 48 | "0: long alphanumeric\n" 49 | "1: short alphanumeric\n" 50 | "2: numeric\n"; 51 | 52 | static const char _copsStatMapping[] = 53 | "0: unknown\n" 54 | "1: available\n" 55 | "2: current\n" 56 | "3: forbidden\n"; 57 | 58 | static const char _umnoProfMapping[] = 59 | "0: default\n" 60 | "1: SIM ICCID select\n" 61 | "2: AT&T\n" 62 | "3: Verizon\n" 63 | "19: Vodafone\n" 64 | "21: Telus\n" 65 | "31: Deutsche Telekom\n" 66 | "100: Manual band selection (or Europe)\n" 67 | "101: Manual band selection (or Europe, no ePCO)\n" 68 | "198: AT&T (no band 5)\n"; 69 | 70 | static const char _cfunMapping[] = 71 | "0: minimum functionality\n" 72 | "1: full functionality\n" 73 | "4: airplane\n" 74 | "15: silent reset\n" 75 | "16: silent reset plus SIM\n" 76 | "19: minimum functionality, deactivate SIM\n" 77 | "127: deep sleep\n"; 78 | 79 | static const char _cfunResetMapping[] = 80 | "0: do not reset before setting fun\n" 81 | "1: silent reset plus SIM before setting fun\n"; 82 | 83 | static const char _cievSignalMapping[] = 84 | "0: < -105 dBm\n" 85 | "1: < -193 dBm\n" 86 | "2: < -81 dBm\n" 87 | "3: < -69 dBm\n" 88 | "4: < -57 dBm\n" 89 | "5: >= -57 dBm\n"; 90 | 91 | static const char _cievGprsMapping[] = 92 | "0: no GPRS available\n" 93 | "1: GPRS available but not registered\n" 94 | "2: registered to GPRS\n"; 95 | 96 | 97 | CellularHelp::CellularHelp() { 98 | } 99 | 100 | 101 | CellularHelp::~CellularHelp() { 102 | } 103 | 104 | void CellularHelp::setup() { 105 | 106 | CellularInterpreter::getInstance()->addModemMonitor( 107 | "CREG|CGREG|CEREG", 108 | CellularInterpreterModemMonitor::REASON_SEND | CellularInterpreterModemMonitor::REASON_PLUS | CellularInterpreterModemMonitor::REASON_URC, 109 | [this](uint32_t reason, const char *cmd, CellularInterpreterModemMonitor *mon) { 110 | // 111 | CellularInterpreterParser parser; 112 | parser.parse(cmd); 113 | 114 | String regType; 115 | if (mon->command.equals("CREG")) { 116 | regType = "Network registration (CREG)"; 117 | } 118 | else 119 | if (mon->command.equals("CGREG")) { 120 | regType = "GPRS network (CGREG)"; 121 | } 122 | if (mon->command.equals("CEREG")) { 123 | regType = "EPS network (CEREG)"; 124 | } 125 | 126 | if ((reason & CellularInterpreterModemMonitor::REASON_SEND) != 0) { 127 | if (parser.getNumArgs() > 0) { 128 | // Set if numArgs > 0 129 | _log.info("%s Set to %s", 130 | regType.c_str(), 131 | CellularInterpreter::mapValueToString(_cregnMapping, parser.getArgInt(0)).c_str()); 132 | } 133 | return; 134 | } 135 | 136 | size_t statArg; 137 | 138 | if ((reason & CellularInterpreterModemMonitor::REASON_PLUS) != 0) { 139 | // Response 140 | // int n = getArgInt(0); 141 | //_log.info("REASON_PLUS: %s", cmd); 142 | statArg = 1; 143 | } 144 | else { 145 | // URC 146 | // _log.info("REASON_URC: %s", cmd); 147 | statArg = 0; 148 | } 149 | 150 | int stat = parser.getArgInt(statArg); 151 | 152 | if (stat == 2 && networkRegStartTime == 0) { 153 | // Searching, not start time 154 | networkRegStartTime = System.millis(); 155 | } 156 | 157 | String statStr = CellularInterpreter::mapValueToString(_cregStatMapping, stat); 158 | 159 | _log.info("%s Status: %s", regType.c_str(), statStr.c_str()); 160 | 161 | if (stat == 1 || stat == 5) { 162 | if (parser.getNumArgs() >= (statArg + 4)) { 163 | _log.info(" Tracking area code: %s", parser.getArgString(statArg +1).c_str()); 164 | _log.info(" Cell identifier: %s", parser.getArgString(statArg + 2).c_str()); 165 | int act = parser.getArgInt(statArg + 3); 166 | String actStr = CellularInterpreter::mapValueToString(_cregActMapping, act); 167 | _log.info(" Access technology: %s", actStr.c_str()); 168 | 169 | if (mon->command.equals("CREG")) { 170 | // Network only 171 | networkRegTime = System.millis(); 172 | } 173 | else { 174 | // CEREG or CGREG (GPRS) 175 | grpsRegTime = System.millis(); 176 | } 177 | } 178 | } 179 | }); 180 | 181 | CellularInterpreter::getInstance()->addModemMonitor( 182 | "COPS", 183 | CellularInterpreterModemMonitor::REASON_SEND | CellularInterpreterModemMonitor::REASON_PLUS, 184 | [](uint32_t reason, const char *cmd, CellularInterpreterModemMonitor *mon) { 185 | // Ignore when trace is disabled 186 | if (!CellularInterpreter::getInstance()->logSettingsTraceEnabled()) { 187 | return; 188 | } 189 | 190 | // 191 | CellularInterpreterParser parser; 192 | parser.parse(cmd); 193 | 194 | if ((reason & CellularInterpreterModemMonitor::REASON_SEND) != 0) { 195 | // Request 196 | // _log.info("COPS SEND: %s", cmd); 197 | if (parser.isSet()) { 198 | _log.info("Operator Selection (COPS) Set: mode=%s format=%s", 199 | CellularInterpreter::mapValueToString(_copsModeMapping, parser.getArgInt(0)).c_str(), 200 | CellularInterpreter::mapValueToString(_copsFormatMapping, parser.getArgInt(1)).c_str()); 201 | } 202 | } 203 | else { 204 | // Response 205 | if (mon->request.isRead()) { 206 | if (parser.getNumArgs() >= 1) { 207 | _log.info("Operator Selection (COPS) Read: mode=%s", 208 | CellularInterpreter::mapValueToString(_copsModeMapping, parser.getArgInt(0)).c_str()); 209 | } 210 | if (parser.getNumArgs() >= 2) { 211 | _log.info(" format=%s", 212 | CellularInterpreter::mapValueToString(_copsFormatMapping, parser.getArgInt(1)).c_str()); 213 | } 214 | if (parser.getNumArgs() >= 3) { 215 | String oper = parser.getArgString(2); 216 | 217 | 218 | if (parser.getArgInt(1) == 2) { 219 | // COPS mode is numeric 220 | int mcc = atoi(oper.substring(0, 3)); 221 | int mnc = atoi(oper.substring(3)); 222 | 223 | String carrier = lookupMccMnc((uint16_t) mcc, (uint16_t) mnc); 224 | String country = lookupCountry((uint16_t) mcc); 225 | _log.info(" oper=%s carrier=%s country=%s", oper.c_str(), carrier.c_str(), country.c_str()); 226 | } 227 | else { 228 | _log.info(" oper=%s", oper.c_str()); 229 | } 230 | } 231 | } 232 | else { 233 | 234 | } 235 | } 236 | 237 | }); 238 | 239 | CellularInterpreter::getInstance()->addModemMonitor( 240 | "UMNOPROF", 241 | CellularInterpreterModemMonitor::REASON_PLUS, 242 | [](uint32_t reason, const char *cmd, CellularInterpreterModemMonitor *mon) { 243 | // 244 | CellularInterpreterParser parser; 245 | parser.parse(cmd); 246 | 247 | int prof = parser.getArgInt(0); 248 | 249 | String profStr = CellularInterpreter::mapValueToString(_umnoProfMapping, prof); 250 | 251 | _log.info("Mobile Network Operator Profile (UMNOPROF): %s", profStr.c_str()); 252 | 253 | 254 | }); 255 | 256 | 257 | CellularInterpreter::getInstance()->addModemMonitor( 258 | "CGDCONT", 259 | CellularInterpreterModemMonitor::REASON_SEND, 260 | [](uint32_t reason, const char *cmd, CellularInterpreterModemMonitor *mon) { 261 | // 262 | CellularInterpreterParser parser; 263 | parser.parse(cmd); 264 | 265 | int cid = parser.getArgInt(0); 266 | 267 | _log.info("PDP context definition (CGDCONT): cid=%d pdpType=%s APN=%s", 268 | cid, parser.getArgString(1).c_str(), parser.getArgString(2).c_str()); 269 | 270 | }); 271 | 272 | CellularInterpreter::getInstance()->addModemMonitor( 273 | "CFUN", 274 | CellularInterpreterModemMonitor::REASON_SEND, 275 | [](uint32_t reason, const char *cmd, CellularInterpreterModemMonitor *mon) { 276 | // 277 | CellularInterpreterParser parser; 278 | parser.parse(cmd); 279 | 280 | _log.info("Set module functionality (CFUN): %s", 281 | CellularInterpreter::mapValueToString(_cfunMapping, parser.getArgInt(0)).c_str()); 282 | if (parser.getNumArgs() > 1) { 283 | _log.info(" Reset Mode: %s", 284 | CellularInterpreter::mapValueToString(_cfunResetMapping, parser.getArgInt(1)).c_str()); 285 | } 286 | }); 287 | 288 | CellularInterpreter::getInstance()->addModemMonitor( 289 | "USOCTL", 290 | CellularInterpreterModemMonitor::REASON_ERROR, 291 | [](uint32_t reason, const char *cmd, CellularInterpreterModemMonitor *mon) { 292 | // 293 | _log.info("operation not allowed is expected here."); 294 | }); 295 | 296 | CellularInterpreter::getInstance()->addModemMonitor( 297 | "CIEV", 298 | CellularInterpreterModemMonitor::REASON_URC, 299 | [](uint32_t reason, const char *cmd, CellularInterpreterModemMonitor *mon) { 300 | // 301 | CellularInterpreterParser parser; 302 | parser.parse(cmd); 303 | 304 | int descr = parser.getArgInt(0); 305 | int value = parser.getArgInt(1); 306 | 307 | switch(descr) { 308 | case 1: 309 | _log.info("CIEV battery charge level 0-5: %d", value); 310 | break; 311 | 312 | case 2: 313 | _log.info("CIEV signal level %s", 314 | CellularInterpreter::mapValueToString(_cievSignalMapping, value).c_str()); 315 | break; 316 | case 3: 317 | _log.info("CIEV service %s registered to network", ((value == 0) ? "not" : "")); 318 | break; 319 | 320 | case 7: 321 | _log.info("CIEV service %s roaming", ((value == 0) ? "not" : "")); 322 | break; 323 | 324 | case 9: 325 | _log.info("CIEV %s", 326 | CellularInterpreter::mapValueToString(_cievGprsMapping, value).c_str()); 327 | break; 328 | 329 | default: 330 | _log.info("CIEV descr=%d value=%d", descr, value); 331 | break; 332 | } 333 | 334 | _log.info("Set module functionality (CFUN): %s", 335 | CellularInterpreter::mapValueToString(_cfunMapping, parser.getArgInt(0)).c_str()); 336 | if (parser.getNumArgs() > 1) { 337 | _log.info(" Reset Mode: %s", 338 | CellularInterpreter::mapValueToString(_cfunResetMapping, parser.getArgInt(1)).c_str()); 339 | } 340 | }); 341 | 342 | CellularInterpreter::getInstance()->addModemMonitor( 343 | "CSQ", 344 | CellularInterpreterModemMonitor::REASON_PLUS, 345 | [](uint32_t reason, const char *cmd, CellularInterpreterModemMonitor *mon) { 346 | // Ignore when trace is disabled 347 | if (!CellularInterpreter::getInstance()->logSettingsTraceEnabled()) { 348 | return; 349 | } 350 | 351 | // 352 | CellularInterpreterParser parser; 353 | parser.parse(cmd); 354 | 355 | int rssi = parser.getArgInt(0); 356 | int qual = parser.getArgInt(1); 357 | 358 | String rssiStr; 359 | switch(rssi) { 360 | case 0: 361 | rssiStr = "<-113 dBm"; 362 | break; 363 | 364 | case 31: 365 | rssiStr = ">=-51 dBm"; 366 | break; 367 | 368 | case 99: 369 | rssiStr = "unknown"; 370 | break; 371 | 372 | default: 373 | if (rssi < 31) { 374 | rssiStr = String::format("%d dBm", -113 + 2 * rssi); 375 | } 376 | else { 377 | rssiStr = String::format("unknown (%d)", rssi); 378 | } 379 | } 380 | 381 | String qualStr; 382 | if (qual <= 7) { 383 | qualStr = String::format("%d (0-7, lower is better)", qual); 384 | } 385 | else { 386 | qualStr = String::format("unknown (%d)", qual); 387 | } 388 | 389 | _log.info("Signal Quality (CSQ): rssi=%s qual=%s", rssiStr.c_str(), qualStr.c_str()); 390 | }); 391 | 392 | } 393 | 394 | void CellularHelp::loop() { 395 | if ((networkRegTime != 0) && (grpsRegTime == 0) && 396 | (networkRegTime < (System.millis() - 15000))) { 397 | // We have network registration (CREG) but not GPRS (CEREG/CGREG), most likely a deactivated SIM 398 | if (System.millis() > deactivatedSimNextReport) { 399 | deactivatedSimNextReport = System.millis() + 60000; 400 | _log.info("Network registered but not GPRS, may be a deactivated SIM"); 401 | } 402 | } 403 | 404 | if ((networkRegStartTime != 0) && (networkRegTime == 0) && (grpsRegTime == 0) && 405 | ((System.millis() - networkRegStartTime) >= 60000)) { 406 | // Searching for network for more than a minute 407 | if (System.millis() > noServiceNextReport) { 408 | noServiceNextReport = System.millis() + 60000; 409 | _log.info("No compatible carriers found (searching for %s)", elapsedString((System.millis() - networkRegStartTime) / 1000).c_str()); 410 | } 411 | 412 | } 413 | 414 | 415 | 416 | } 417 | 418 | -------------------------------------------------------------------------------- /src/CellularHelp.h: -------------------------------------------------------------------------------- 1 | #ifndef __CELLULARHELP_H 2 | #define __CELLULARHELP_H 3 | 4 | #include "Particle.h" 5 | 6 | /** 7 | * @brief Generate helpful messages to help decode the cellular commands, particularly for connection failures 8 | * 9 | * Just call CellularInterpreterHelpCellularConnection::check() from setup() to enable after calling 10 | * CellularInterpreter::setup(). 11 | * 12 | * This is only intended for human-readable applications. 13 | */ 14 | class CellularHelp { 15 | public: 16 | CellularHelp(); 17 | virtual ~CellularHelp(); 18 | 19 | void setup(); 20 | 21 | void loop(); 22 | 23 | protected: 24 | uint64_t networkRegStartTime = 0; 25 | uint64_t networkRegTime = 0; 26 | uint64_t grpsRegTime = 0; 27 | uint64_t deactivatedSimNextReport = 0; 28 | uint64_t noServiceNextReport = 0; 29 | }; 30 | 31 | 32 | #endif /* __CELLULARHELP_H */ 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/CellularInterpreterRK.cpp: -------------------------------------------------------------------------------- 1 | #include "CellularInterpreterRK.h" 2 | 3 | static Logger _log("app.cellinterp"); 4 | 5 | CellularInterpreter *CellularInterpreter::instance = 0; 6 | CellularInterpreterBlinkManager *CellularInterpreterBlinkManager::instance = 0; 7 | 8 | CellularInterpreterParser::CellularInterpreterParser() { 9 | 10 | } 11 | CellularInterpreterParser::~CellularInterpreterParser() { 12 | 13 | } 14 | 15 | void CellularInterpreterParser::clear() { 16 | requestType = RequestType::UNKNOWN_REQUEST; 17 | command = ""; 18 | args.clear(); 19 | } 20 | 21 | void CellularInterpreterParser::parse(const char *commandLineIn) { 22 | String curPart; 23 | bool inQuote = false; 24 | bool isAT = false; 25 | bool isPlus = false; 26 | 27 | if (strncmp(commandLineIn, "AT", 2) == 0) { 28 | isAT = true; 29 | } 30 | else if (commandLineIn[0] == '+') { 31 | isPlus = true; 32 | } 33 | 34 | for(const char *cp = commandLineIn; *cp; cp++) { 35 | if (isAT) { 36 | if (*cp == '=' || *cp == '?') { 37 | // AT command ends at = or ? 38 | command = curPart; 39 | curPart = ""; 40 | 41 | if (*cp == '?') { 42 | requestType = RequestType::READ_REQUEST; 43 | } 44 | else { 45 | if (cp[1] == '?') { 46 | // =? 47 | requestType = RequestType::TEST_REQUEST; 48 | cp++; 49 | } 50 | else { 51 | // = 52 | requestType = RequestType::SET_REQUEST; 53 | } 54 | } 55 | 56 | isAT = false; 57 | } 58 | } 59 | else if (isPlus) { 60 | if (*cp == ':') { 61 | command = curPart; 62 | curPart = ""; 63 | isPlus = false; 64 | if (cp[1] == ' ') { 65 | // Skip space after : as well 66 | cp++; 67 | } 68 | } 69 | } 70 | else if (*cp == '"') { 71 | inQuote = !inQuote; 72 | } 73 | else if (!inQuote && *cp == ',') { 74 | // Comma separator, not in a quote 75 | args.push_back(curPart); 76 | curPart = ""; 77 | } 78 | else { 79 | // Include in this string 80 | curPart += *cp; 81 | } 82 | 83 | } 84 | if (curPart.length() != 0) { 85 | args.push_back(curPart); 86 | } 87 | } 88 | 89 | String CellularInterpreterParser::getArgString(size_t index) const { 90 | if (index < args.size()) { 91 | return args[index]; 92 | } 93 | else { 94 | return ""; 95 | } 96 | } 97 | 98 | int CellularInterpreterParser::getArgInt(size_t index) const { 99 | return atoi(getArgString(index)); 100 | } 101 | 102 | long CellularInterpreterParser::getArgLongHex(size_t index) const { 103 | char *end; 104 | return strtol(getArgString(index).c_str(), &end, 16); 105 | } 106 | 107 | 108 | CellularInterpreter::CellularInterpreter() : StreamLogHandler(*this, LOG_LEVEL_TRACE) { 109 | instance = this; 110 | } 111 | 112 | CellularInterpreter::~CellularInterpreter() { 113 | // TODO: Deallocate buffers 114 | 115 | // TODO: Deallocate objects in commandMonitors 116 | } 117 | 118 | void CellularInterpreter::setup() { 119 | 120 | // Add this callback into the system log manager 121 | LogManager::instance()->addHandler(this); 122 | 123 | } 124 | 125 | void CellularInterpreter::loop() { 126 | // We skip over logs generated from this thread to prevent recursively 127 | // generating logs 128 | loopThread = os_thread_current(NULL); 129 | 130 | while(!queue.empty()) { 131 | CellularInterpreterQueueEntry *entry = queue.front(); 132 | queue.pop_front(); 133 | 134 | if (entry->entryType == CellularInterpreterQueueEntry::EntryType::LOG_ENTRY) { 135 | processLog(entry); 136 | } 137 | else 138 | if (entry->entryType == CellularInterpreterQueueEntry::EntryType::MODEM_ENTRY) { 139 | processCommand(entry); 140 | } 141 | else 142 | if (entry->entryType == CellularInterpreterQueueEntry::EntryType::LOG_SETTINGS) { 143 | logSettings &= entry->andMask; 144 | logSettings |= entry->orMask; 145 | } 146 | 147 | 148 | delete entry; 149 | } 150 | 151 | processTimeouts(); 152 | 153 | loopThread = NULL; 154 | } 155 | 156 | void CellularInterpreter::blinkNotification(uint32_t timesColor, size_t repeats) { 157 | 158 | uint32_t color = (timesColor & 0x00ffffff); 159 | size_t times = (timesColor >> 24) & 0x0f; 160 | 161 | CellularInterpreterBlinkManager *blinkManager = CellularInterpreterBlinkManager::getInstance(); 162 | if (blinkManager) { 163 | CellularInterpreterBlinkPatternBlink *pat = new CellularInterpreterBlinkPatternBlink(); 164 | if (pat) { 165 | pat->withSlowBlink(color, times).withRepeats(repeats); 166 | blinkManager->addBlinkPattern(pat); 167 | } 168 | } 169 | 170 | } 171 | 172 | 173 | void CellularInterpreter::addLogMonitor(CellularInterpreterLogMonitor *mon) { 174 | logMonitors.push_back(mon); 175 | } 176 | 177 | void CellularInterpreter::addLogMonitor(const char *category, const char *level, const char *matchString, CellularInterpreterLogCallback callback) { 178 | CellularInterpreterLogMonitor *mon = new CellularInterpreterLogMonitor(); 179 | 180 | if (category) { 181 | mon->category = category; 182 | } 183 | if (level) { 184 | mon->level = level; 185 | } 186 | if (matchString) { 187 | mon->matchString = matchString; 188 | } 189 | mon->callback = callback; 190 | 191 | addLogMonitor(mon); 192 | } 193 | 194 | 195 | void CellularInterpreter::addModemMonitor(CellularInterpreterModemMonitor *mon) { 196 | commandMonitors.push_back(mon); 197 | } 198 | 199 | void CellularInterpreter::addModemMonitor(const char *cmdName, uint32_t reasonFlags, CellularInterpreterModemCallback callback, unsigned long timeout) { 200 | char *cmdNameMutable = strdup(cmdName); 201 | if (cmdNameMutable) { 202 | char *endptr = 0; 203 | char *token = strtok_r(cmdNameMutable, "|", &endptr); 204 | while(token) { 205 | CellularInterpreterModemMonitor *mon = new CellularInterpreterModemMonitor(); 206 | mon->command = token; 207 | mon->reasonFlags = reasonFlags; 208 | mon->timeout = timeout; 209 | mon->callback = callback; 210 | 211 | addModemMonitor(mon); 212 | 213 | token = strtok_r(NULL, "|", &endptr); 214 | } 215 | 216 | free(cmdNameMutable); 217 | } 218 | } 219 | 220 | 221 | void CellularInterpreter::addUrcHandler(const char *urc, CellularInterpreterModemCallback callback) { 222 | CellularInterpreterModemMonitor *mon = new CellularInterpreterModemMonitor(); 223 | mon->command = urc; 224 | mon->reasonFlags = CellularInterpreterModemMonitor::REASON_URC; 225 | mon->timeout = 0; 226 | mon->callback = callback; 227 | 228 | addModemMonitor(mon); 229 | } 230 | 231 | 232 | size_t CellularInterpreter::write(uint8_t c) { 233 | // Do not add any _log.* statements in this function! 234 | 235 | if ((logSettings & LOG_LITERAL) != 0) { 236 | // Output everthing exactly as sent as if this module didn't exist at all 237 | logOutput(c); 238 | } 239 | 240 | if (loopThread && os_thread_is_current(loopThread)) { 241 | if ((logSettings & LOG_LITERAL) == 0) { 242 | logOutput(c); 243 | } 244 | return 1; 245 | } 246 | 247 | if ((char)c == '\r') { 248 | // End of line, process this line 249 | 250 | // offset will be at most WRITE_BUF_SIZE - 1 251 | // so this is safe to null terminate the (possibly partial) line 252 | if (writeOffset > 0) { 253 | writeBuffer[writeOffset] = 0; 254 | 255 | processLine(writeBuffer); 256 | 257 | writeOffset = 0; 258 | } 259 | writeOffset = 0; 260 | } 261 | else 262 | if ((char) c == '\n') { 263 | // Ignore LFs 264 | } 265 | else 266 | if (writeOffset < (CellularInterpreter::WRITE_BUF_SIZE - 1)) { 267 | // Append other characters to the line if there's room 268 | writeBuffer[writeOffset++] = (char) c; 269 | } 270 | else { 271 | // Line is too long, ignore the rest 272 | } 273 | return 1; 274 | } 275 | 276 | void CellularInterpreter::logOutput(uint8_t c) { 277 | if ((logSettings & LOG_SERIAL) != 0) { 278 | Serial.write(c); 279 | } 280 | if ((logSettings & LOG_SERIAL1) != 0) { 281 | Serial1.write(c); 282 | } 283 | } 284 | 285 | void CellularInterpreter::logOutput(const char *s) { 286 | while(*s) { 287 | logOutput(*s++); 288 | } 289 | } 290 | 291 | uint32_t CellularInterpreter::updateLogSettings(uint32_t andMask, uint32_t orMask) { 292 | logSettings &= andMask; 293 | logSettings |= orMask; 294 | 295 | return logSettings; 296 | } 297 | 298 | 299 | 300 | void CellularInterpreter::processLine(char *lineBuffer) { 301 | // Process the (possibly partial) line in lineBuffer. It's a c-string, always null 302 | // terminated even if the line is truncated. 303 | // You only get the beginning of the line, the truncated part is discarded and 304 | // never copied into lineBuffer. 305 | 306 | // Example: Gen 2 307 | // 50.750 AT send 4 "AT\r\n" 308 | // 50.753 AT read OK 6 "\r\nOK\r\n" 309 | // 50.753 AT send 13 "AT+COPS=3,2\r\n" 310 | // 50.757 AT read OK 6 "\r\nOK\r\n" 311 | // 50.757 AT send 10 "AT+COPS?\r\n" 312 | // 50.763 AT read + 25 "\r\n+COPS: 0,2,\"310410\",2\r\n" 313 | // 50.764 AT read OK 6 "\r\nOK\r\n" 314 | // 50.764 AT send 8 "AT+CSQ\r\n" 315 | // 50.768 AT read + 14 "\r\n+CSQ: 16,2\r\n" 316 | // 50.769 AT read OK 6 "\r\nOK\r\n" 317 | // col=0 token=50.750 318 | // col=1 token=AT 319 | // col=2 token=send 320 | // col=3 token=4 321 | // col=4 token="AT 322 | // 323 | // Example with binary data 324 | // 11.037 AT read + 78 "\r\n+USORF: 0,\"54.86.198.203\",5684,38,\"\x17\xfe\xfd\x00\x01\x00\x00\x00\x00\x00\x9f\x00\x19\x00\x01\x00\x00\x00\x00\x00\x9f\xd0'R@\xf594Rf\xe0g\x89\x85'S\x99\xca\"\r\n" 325 | // 326 | 327 | 328 | // Example: Gen 3 329 | // 0000068021 [ncp.at] TRACE: > AT+COPS=3,2 330 | // 0000068029 [ncp.at] TRACE: < OK 331 | // 0000068029 [ncp.at] TRACE: > AT+COPS? 332 | // 0000068039 [ncp.at] TRACE: < +COPS: 0,2,"310410",8 333 | // 0000068040 [ncp.at] TRACE: < OK 334 | // 0000068040 [ncp.at] TRACE: > AT+UCGED=5 335 | // 0000068049 [ncp.at] TRACE: < OK 336 | // 0000068049 [ncp.at] TRACE: > AT+UCGED? 337 | // 0000068062 [ncp.at] TRACE: < +RSRP: 476,5110,"-096.50", 338 | // 0000068062 [ncp.at] TRACE: < +RSRQ: 476,5110,"-20.00", 339 | // 0000068063 [ncp.at] TRACE: < OK 340 | // col=0 token=0000068021 341 | // col=1 token=[ncp.at] 342 | // col=2 token=TRACE: 343 | // col=3 token=> 344 | // col=4 token=AT+COPS=3,2 345 | 346 | 347 | /* TODO: Handle this sort of response better 348 | 0000004409 [ncp.at] TRACE: > AT+UGPIOC? 349 | 0000004410 [app.cellinterp] INFO: recv OK 350 | 0000004411 [app] INFO: processCommand command=AT+UGPIOC? toModem=1 351 | 0000004412 [app.cellinterp] INFO: send command AT+UGPIOC? 352 | 0000004422 [ncp.at] TRACE: < +UGPIOC: 353 | 0000004423 [ncp.at] TRACE: < 16,255 354 | 0000004423 [ncp.at] TRACE: < 19,255 355 | 0000004424 [ncp.at] TRACE: < 23,0 356 | 0000004425 [ncp.at] TRACE: < 24,255 357 | 0000004426 [ncp.at] TRACE: < 25,255 358 | 0000004426 [ncp.at] TRACE: < 42,255 359 | 0000004427 [ncp.at] TRACE: < OK 360 | 0000004428 [ncp.at] TRACE: > AT+UGPIOR=23 361 | 0000004430 [app] INFO: processCommand command=+UGPIOC: toModem=0 362 | 0000004430 [app.cellinterp] INFO: recv + +UGPIOC: 363 | 0000004431 [app] INFO: processCommand command=16,255 toModem=0 364 | 0000004432 [app] INFO: processCommand command=19,255 toModem=0 365 | 0000004433 [app] INFO: processCommand command=23,0 toModem=0 366 | */ 367 | 368 | // _log.info("processLine %s", lineBuffer); 369 | 370 | const size_t MAX_COL_TOKENS = 4; 371 | char *colTokens[MAX_COL_TOKENS]; 372 | char *msg; 373 | 374 | bool isGen3 = false; 375 | bool isLogger = false; 376 | long ts = 0; 377 | char *saveptr; 378 | 379 | CellularInterpreterQueueEntry *entry = NULL; 380 | 381 | char *token = strtok_r(lineBuffer, " ", &saveptr); 382 | for(size_t col = 0; token; col++) { 383 | if (col < MAX_COL_TOKENS) { 384 | colTokens[col] = token; 385 | } 386 | if (col == 0) { 387 | if (strcmp(token, "Socket") == 0) { 388 | // Gen2: Socket 0: handle 0 has 33 bytes pending 389 | if ((logSettings & LOG_VERBOSE) == 0) { 390 | // Ignore this message 391 | break; 392 | } 393 | // If LOG_FULL_AT_CMD are being logged, we'll output this 394 | // in the col == 1 case below. 395 | } 396 | } 397 | if (col == 1) { 398 | if (strcmp(token, "AT") == 0) { 399 | isGen3 = false; 400 | } 401 | else 402 | if (strcmp(token, "[ncp.at]") == 0) { 403 | isGen3 = true; 404 | } 405 | else 406 | if (token[0] == '[') { 407 | isLogger = true; 408 | } 409 | else { 410 | // Not a statement we care about, ignore the rest of this line. 411 | 412 | /* 413 | char *msg = &token[strlen(token) + 1]; 414 | if ((logSettings & LOG_LITERAL) == 0) { 415 | logOutput(colTokens[0]); 416 | logOutput(' '); 417 | logOutput(colTokens[1]); 418 | logOutput(' '); 419 | logOutput(msg); 420 | logOutput('\n'); 421 | } 422 | */ 423 | break; 424 | } 425 | } 426 | 427 | if (isLogger || isGen3) { 428 | if (col == 2) { 429 | // Clean up category and level 430 | char *cp; 431 | 432 | // Rest of the line after this token is the message 433 | msg = &token[strlen(token) + 1]; 434 | 435 | // Remove the square brackets from category 436 | if (colTokens[1][0] == '[') { 437 | cp = strrchr(colTokens[1], ']'); 438 | if (cp) { 439 | ++colTokens[1]; 440 | *cp = 0; 441 | } 442 | } 443 | 444 | // Remove the : from the end of the level 445 | cp = strrchr(colTokens[2], ':'); 446 | if (cp) { 447 | *cp = 0; 448 | } 449 | 450 | char *end; 451 | ts = strtol(colTokens[0], &end, 10); 452 | } 453 | } 454 | 455 | if (isLogger) { 456 | // Non-modem logging messages: 457 | // 0000011877 [hal] ERROR: Failed to power off modem 458 | // 0000011878 [hal] TRACE: Modem already on 459 | // 0000011879 [hal] TRACE: Setting UART voltage translator state 1 460 | 461 | // 0: Timestamp 462 | // 1: Category ([hal], [app], etc.) 463 | // 2: Level (TRACE, INFO, ERROR etc.) 464 | // 3: rest of the message 465 | if (col == 2) { 466 | 467 | // _log.info("logger found 0:%s 1:%s 2:%s msg:%s", colTokens[0], colTokens[1], colTokens[2], msg); 468 | entry = new CellularInterpreterQueueEntry(); 469 | entry->entryType = CellularInterpreterQueueEntry::EntryType::LOG_ENTRY; 470 | 471 | entry->ts = ts; 472 | entry->category = colTokens[1]; 473 | entry->level = colTokens[2]; 474 | entry->message = msg; 475 | break; 476 | } 477 | } 478 | else 479 | if (isGen3) { 480 | // Gen3 481 | if (col == 3) { 482 | // 0000068021 [ncp.at] TRACE: > AT+COPS=3,2 483 | // 0000068029 [ncp.at] TRACE: < OK 484 | 485 | entry = new CellularInterpreterQueueEntry(); 486 | entry->entryType = CellularInterpreterQueueEntry::EntryType::MODEM_ENTRY; 487 | 488 | entry->ts = ts; 489 | entry->category = colTokens[1]; 490 | entry->level = colTokens[2]; 491 | 492 | entry->toModem = token[0] == '>'; 493 | entry->message = &token[2]; 494 | break; 495 | } 496 | } 497 | else { 498 | // Gen2 499 | if (col == 2) { 500 | entry = new CellularInterpreterQueueEntry(); 501 | entry->entryType = CellularInterpreterQueueEntry::EntryType::MODEM_ENTRY; 502 | entry->toModem = strcmp(token, "send") == 0; 503 | 504 | int tsWhole, tsDec = 0; 505 | String s = colTokens[0]; 506 | int index = s.indexOf('.'); 507 | if (index >= 0) { 508 | tsWhole = atoi(s.substring(0, index)); 509 | tsDec = atoi(s.substring(index + 1)); 510 | } 511 | else { 512 | tsWhole = atoi(s); 513 | } 514 | 515 | entry->ts = (long) (tsWhole * 1000) + (tsDec % 1000); 516 | entry->category = "ncp.at"; 517 | entry->level = "TRACE"; 518 | 519 | char *src = strchr(&token[strlen(token) + 1], '"'); 520 | if (src) { 521 | // Command begins after the first double quote 522 | const char *command = ++src; 523 | 524 | // Unescape backslash escapes, remove \r and \n, and end at the unescaped double quote 525 | for(char *dst = src; *src; src++) { 526 | if (*src == '\\') { 527 | if (*++src == '"') { 528 | // Literal quote 529 | *dst++ = *src; 530 | } 531 | else { 532 | // Probably \r or \n, just ignore 533 | } 534 | } 535 | else 536 | if (*src == '"') { 537 | // End of string 538 | *dst = 0; 539 | break; 540 | } 541 | else { 542 | // Copy character 543 | *dst++ = *src; 544 | } 545 | } 546 | 547 | // Truncate command to a reasonable length? 548 | if (strcmp(command, "AT") == 0) { 549 | // Empty command 550 | ignoreNextOK = true; 551 | delete entry; 552 | entry = 0; 553 | break; 554 | } 555 | else if (strcmp(command, "OK") == 0 && ignoreNextOK) { 556 | ignoreNextOK = false; 557 | delete entry; 558 | entry = 0; 559 | break; 560 | } 561 | 562 | entry->message = command; 563 | } 564 | break; 565 | } 566 | } 567 | 568 | token = strtok_r(NULL, " ", &saveptr); 569 | } 570 | 571 | if (entry) { 572 | queue.push_back(entry); 573 | } 574 | 575 | } 576 | 577 | void CellularInterpreter::queueLogSettings(uint32_t andMask, uint32_t orMask) { 578 | CellularInterpreterQueueEntry *entry = new CellularInterpreterQueueEntry(); 579 | entry->entryType = CellularInterpreterQueueEntry::EntryType::LOG_SETTINGS; 580 | 581 | entry->andMask = andMask; 582 | entry->orMask = orMask; 583 | 584 | queue.push_back(entry); 585 | } 586 | 587 | 588 | 589 | void CellularInterpreter::logCommand(CellularInterpreterQueueEntry *entry) { 590 | if (((logSettings & LOG_LITERAL) == 0) && ((logSettings & LOG_TRACE) != 0)) { 591 | String msg = String::format("%010ld [%s] %s: %s %s\n", entry->ts, entry->category.c_str(), entry->level.c_str(), (entry->toModem ? ">" : "<"), entry->message.c_str()); 592 | logOutput(msg); 593 | } 594 | } 595 | 596 | bool CellularInterpreter::includeCommandInLog(const char *cmd) const { 597 | if ((logSettings & LOG_VERBOSE) == 0) { 598 | return !isMatchingCommand(cmd, "UUSORD") && !isMatchingCommand(cmd, "USORF") && !isMatchingCommand(cmd, "UUSORF") && !isMatchingCommand(cmd, "USOST"); 599 | } 600 | else { 601 | // Show all commands in verbose 602 | return true; 603 | } 604 | } 605 | 606 | void CellularInterpreter::processCommand(CellularInterpreterQueueEntry *entry) { 607 | 608 | // Note start of new command, + responses, OK/ERROR responses, and URCs here 609 | 610 | if (entry->toModem) { 611 | // Sending to modem 612 | if (ignoreNextSend) { 613 | // On Gen2, sending binary data with AT+USOST or AT+USOWR, see "@" below 614 | ignoreNextSend = false; 615 | return; 616 | } 617 | // _log.info("send command %s", command); 618 | if (includeCommandInLog(entry->message)) { 619 | logCommand(entry); 620 | } 621 | callCommandMonitors(CellularInterpreterModemMonitor::REASON_SEND, entry->message); 622 | lastCommand = entry->message; 623 | } 624 | else { 625 | // Receiving data from modem 626 | if (strcmp(entry->message, "OK") == 0) { 627 | // _log.info("recv OK lastCommand=%s", lastCommand.c_str()); 628 | if (includeCommandInLog(lastCommand)) { 629 | logCommand(entry); 630 | } 631 | callCommandMonitors(CellularInterpreterModemMonitor::REASON_OK, lastCommand); 632 | lastCommand = ""; 633 | } 634 | else 635 | if (strncmp(entry->message, "ERROR", 5) == 0 || strncmp(entry->message, "+CME ERROR", 10) == 0) { 636 | // _log.info("recv ERROR lastCommand=%s", lastCommand.c_str()); 637 | if (includeCommandInLog(lastCommand)) { 638 | logCommand(entry); 639 | } 640 | callCommandMonitors(CellularInterpreterModemMonitor::REASON_ERROR, lastCommand); 641 | lastCommand = ""; 642 | } 643 | else 644 | if (entry->message.charAt(0) == '+') { 645 | // + response to a command, or a URC 646 | // _log.info("recv + or URC %s", command); 647 | if (includeCommandInLog(lastCommand) && includeCommandInLog(entry->message)) { 648 | logCommand(entry); 649 | } 650 | callCommandMonitors(CellularInterpreterModemMonitor::REASON_PLUS, entry->message); 651 | } 652 | else 653 | if (strcmp(entry->message, "@") == 0) { 654 | // On Gen2, sending binary data with AT+USOST or AT+USOWR 655 | 656 | // Example with write binary data after @ response (AT+USOST, AT+USOWR) 657 | // 5.565 AT send 36 "AT+USOST=0,\"54.86.198.203\",5684,50\r\n" 658 | // 5.573 AT read > 3 "\r\n@" 659 | // 5.573 AT send 50 "\x17\xfe\xfd\x00\x01\x00\x00\x00\x00\x00\x99\x00%\x00\x01\x00\x00\x00\x00\x00\x99.\xcc\x9dz\xec\xd54\xeb\x87\xbd{\xb2\xc0$}\x19\xf4\x11\xfc\x85\t\xb4\xe8\xae\xe5\xa6\x0e|\x15" 660 | // 5.709 AT read + 16 "\r\n+USOST: 0,50\r\n" 661 | ignoreNextSend = true; 662 | } 663 | else { 664 | // There are a bunch of other responses here like: 665 | // CONNECT, RING, NO CARRIER, NO DIALTONE, BUSY, NO ANSWER, CONNECT, ABORTED 666 | // however we don't need to handle those, so just ignore them 667 | } 668 | 669 | } 670 | } 671 | 672 | void CellularInterpreter::callCommandMonitors(uint32_t reasonFlags, const char *command) { 673 | // Check command monitors 674 | for (std::vector::iterator it = commandMonitors.begin() ; it != commandMonitors.end(); ++it) { 675 | CellularInterpreterModemMonitor *mon = *it; 676 | if (isMatchingCommand(command, mon->command)) { 677 | // Matching command or URC 678 | if ((reasonFlags & CellularInterpreterModemMonitor::REASON_PLUS) != 0) { 679 | // A + can be a URC if there was no send 680 | if (mon->nextTimeout == 0) { 681 | // Yes, it's a URC, change the reason from PLUS to URC 682 | // _log.info("converting PLUS to URC %s", command); 683 | reasonFlags &= ~CellularInterpreterModemMonitor::REASON_PLUS; 684 | reasonFlags |= CellularInterpreterModemMonitor::REASON_URC; 685 | } 686 | } 687 | 688 | if ((reasonFlags & mon->reasonFlags) != 0) { 689 | // Handler is interested in this reason 690 | if (mon->callback) { 691 | mon->callback(reasonFlags, command, mon); 692 | } 693 | } 694 | 695 | if ((reasonFlags & CellularInterpreterModemMonitor::REASON_SEND) != 0) { 696 | // Sending a command, start timeout 697 | mon->request.clear(); 698 | mon->request.parse(command); 699 | if (mon->timeout) { 700 | mon->nextTimeout = System.millis() + mon->timeout; 701 | } 702 | else { 703 | mon->nextTimeout = 0; 704 | } 705 | } 706 | else 707 | if ((reasonFlags & (CellularInterpreterModemMonitor::REASON_OK | CellularInterpreterModemMonitor::REASON_ERROR)) != 0) { 708 | // On OK or ERROR, clear timeout 709 | // _log.info("clearing timeout on %s", mon->command.c_str()); 710 | mon->nextTimeout = 0; 711 | } 712 | } 713 | } 714 | } 715 | 716 | 717 | void CellularInterpreter::processTimeouts() { 718 | for (std::vector::iterator it = commandMonitors.begin() ; it != commandMonitors.end(); ++it) { 719 | CellularInterpreterModemMonitor *mon = *it; 720 | 721 | if (mon->nextTimeout != 0 && mon->nextTimeout < System.millis()) { 722 | // Timeout occurred 723 | _log.info("timeout %s", mon->command.c_str()); 724 | callCommandMonitors(CellularInterpreterModemMonitor::REASON_TIMEOUT, mon->command); 725 | 726 | mon->nextTimeout = 0; 727 | } 728 | } 729 | 730 | } 731 | 732 | void CellularInterpreter::processLog(CellularInterpreterQueueEntry *entry) { 733 | // long ts, const char *category, const char *level, const char *msg 734 | // entry->ts, entry->category, entry->level, entry->message 735 | // _log.info("processLog testing ts=%ld category=%s level=%s msg=%s", ts, category, level, msg); 736 | 737 | if ((logSettings & LOG_LITERAL) == 0) { 738 | String msg = String::format("%010ld [%s] %s: %s\n", entry->ts, entry->category.c_str(), entry->level.c_str(), entry->message.c_str()); 739 | logOutput(msg); 740 | } 741 | 742 | for (std::vector::iterator it = logMonitors.begin() ; it != logMonitors.end(); ++it) { 743 | CellularInterpreterLogMonitor *mon = *it; 744 | 745 | bool match = true; 746 | if (match && mon->category.length() > 0 && !mon->category.equals(entry->category)) { 747 | match = false; 748 | } 749 | if (match && mon->level.length() > 0 && !mon->level.equals(entry->level)) { 750 | match = false; 751 | } 752 | if (match && mon->matchString.length() > 0 && strstr(entry->message, mon->matchString.c_str()) == 0) { 753 | match = false; 754 | } 755 | if (match) { 756 | _log.info("processLog match ts=%ld category=%s level=%s msg=%s", entry->ts, entry->category.c_str(), entry->level.c_str(), entry->message.c_str()); 757 | mon->callback(entry->ts, entry->category, entry->level, entry->message); 758 | } 759 | } 760 | } 761 | 762 | // [static] 763 | bool CellularInterpreter::isMatchingCommand(const char *test, const char *cmd) { 764 | size_t start = 0; 765 | if (test[0] == '+') { 766 | // Plus response or URC 767 | start = 1; 768 | } 769 | else 770 | if (strncmp(test, "AT+", 3) == 0) { 771 | // Sending command 772 | start = 3; 773 | } 774 | 775 | size_t cmdLen = strlen(cmd); 776 | 777 | if (strncmp(&test[start], cmd, cmdLen) == 0) { 778 | switch(test[start + cmdLen]) { 779 | case 0: 780 | case '=': 781 | case '+': 782 | case '?': 783 | case ':': 784 | // Is a match! 785 | return true; 786 | 787 | default: 788 | break; 789 | } 790 | } 791 | return false; 792 | } 793 | 794 | // [static] 795 | String CellularInterpreter::mapValueToString(const char *mapping, int value) { 796 | 797 | for(const char *cp = mapping; *cp;) { 798 | String numStr; 799 | while(*cp && *cp != ':') { 800 | numStr += *cp++; 801 | } 802 | if (!*cp++) { 803 | break; 804 | } 805 | while(*cp && *cp == ' ') { 806 | cp++; 807 | } 808 | if (!*cp) { 809 | break; 810 | } 811 | 812 | int num = atoi(numStr); 813 | 814 | // _log.info("testing numStr=%s num=%d", numStr.c_str(), value); 815 | 816 | if (num == value) { 817 | String desc; 818 | 819 | // This is what we're looking for 820 | while(*cp && *cp != '\n') { 821 | desc += *cp++; 822 | } 823 | return String::format("%s (%d)", desc.c_str(), value); 824 | } 825 | // Skip to next 826 | while(*cp && *cp++ != '\n') { 827 | } 828 | } 829 | // Unknown code 830 | return String::format("unknown %d", value); 831 | } 832 | 833 | 834 | 835 | CellularInterpreterBlinkPattern::CellularInterpreterBlinkPattern() { 836 | } 837 | 838 | CellularInterpreterBlinkPattern::~CellularInterpreterBlinkPattern() { 839 | } 840 | 841 | void CellularInterpreterBlinkPattern::callStart() { 842 | curRepeat = 0; 843 | start(); 844 | } 845 | 846 | bool CellularInterpreterBlinkPattern::callRun() { 847 | bool done = false; 848 | 849 | if (run()) { 850 | // Done with sequence. Should we repeat? 851 | if (++curRepeat < repeats) { 852 | // Yes 853 | start(); 854 | } 855 | else { 856 | // No, really done now 857 | done = true; 858 | } 859 | } 860 | return done; 861 | } 862 | 863 | // [static] 864 | void CellularInterpreterBlinkPattern::setColor(uint32_t color) { 865 | uint8_t r, g, b; 866 | 867 | r = (uint8_t)(color >> 16); 868 | g = (uint8_t)(color >> 8); 869 | b = (uint8_t)color; 870 | 871 | RGB.color(r, g, b); 872 | } 873 | 874 | 875 | CellularInterpreterBlinkPatternBlink::CellularInterpreterBlinkPatternBlink() { 876 | 877 | } 878 | 879 | CellularInterpreterBlinkPatternBlink::~CellularInterpreterBlinkPatternBlink() { 880 | } 881 | 882 | CellularInterpreterBlinkPatternBlink &CellularInterpreterBlinkPatternBlink::withSlowBlink(uint32_t color, size_t blinks) { 883 | this->onColor = color; 884 | this->blinks = blinks; 885 | return *this; 886 | } 887 | 888 | 889 | void CellularInterpreterBlinkPatternBlink::start() { 890 | stateHandler = &CellularInterpreterBlinkPatternBlink::stateStart; 891 | } 892 | 893 | bool CellularInterpreterBlinkPatternBlink::run() { 894 | return stateHandler(*this); 895 | } 896 | 897 | bool CellularInterpreterBlinkPatternBlink::stateStart() { 898 | curBlink = 0; 899 | // RGB.control will be enabled and the LED is off when start() is called 900 | stateTime = millis(); 901 | stateHandler = &CellularInterpreterBlinkPatternBlink::stateBeforeOff; 902 | 903 | return false; 904 | } 905 | bool CellularInterpreterBlinkPatternBlink::stateBeforeOff() { 906 | if (millis() - stateTime >= beforeOffMs) { 907 | setColor(onColor); 908 | stateTime = millis(); 909 | stateHandler = &CellularInterpreterBlinkPatternBlink::stateOn; 910 | } 911 | return false; 912 | } 913 | bool CellularInterpreterBlinkPatternBlink::stateOn() { 914 | if (millis() - stateTime >= onPeriodMs) { 915 | setColor(offColor); 916 | stateTime = millis(); 917 | stateHandler = &CellularInterpreterBlinkPatternBlink::stateOff; 918 | } 919 | 920 | return false; 921 | } 922 | bool CellularInterpreterBlinkPatternBlink::stateOff() { 923 | if (millis() - stateTime >= offPeriodMs) { 924 | stateTime = millis(); 925 | if (++curBlink < blinks) { 926 | setColor(onColor); 927 | stateHandler = &CellularInterpreterBlinkPatternBlink::stateOn; 928 | } 929 | else { 930 | stateHandler = &CellularInterpreterBlinkPatternBlink::stateAfterOff; 931 | } 932 | } 933 | 934 | return false; 935 | } 936 | bool CellularInterpreterBlinkPatternBlink::stateAfterOff() { 937 | if (millis() - stateTime >= afterOffMs) { 938 | // Done! 939 | return true; 940 | } 941 | 942 | return false; 943 | } 944 | 945 | 946 | CellularInterpreterBlinkManager::CellularInterpreterBlinkManager() { 947 | instance = this; 948 | } 949 | 950 | CellularInterpreterBlinkManager::~CellularInterpreterBlinkManager() { 951 | 952 | } 953 | 954 | void CellularInterpreterBlinkManager::setup() { 955 | } 956 | 957 | void CellularInterpreterBlinkManager::loop() { 958 | if (curPattern == 0 && !patternQueue.empty()) { 959 | curPattern = patternQueue.front(); 960 | patternQueue.pop_front(); 961 | 962 | // About to start a pattern. Set direct RGB control and turn the LED off (color = black) 963 | RGB.control(true); 964 | CellularInterpreterBlinkPattern::setColor(0x000000); 965 | 966 | curPattern->callStart(); 967 | } 968 | if (curPattern) { 969 | if (curPattern->callRun()) { 970 | // Done with this pattern 971 | delete curPattern; 972 | curPattern = 0; 973 | if (patternQueue.empty()) { 974 | // No more patterns to display, restore default RGB behavior 975 | RGB.control(false); 976 | } 977 | } 978 | } 979 | } 980 | 981 | void CellularInterpreterBlinkManager::addBlinkPattern(CellularInterpreterBlinkPattern *pat) { 982 | patternQueue.push_back(pat); 983 | } 984 | 985 | /* 986 | RGB_COLOR_BLUE : blue (0x000000ff) 987 | RGB_COLOR_GREEN : green (0x0000ff00) 988 | RGB_COLOR_CYAN : cyan (0x0000ffff) 989 | RGB_COLOR_RED : red (0x00ff0000) 990 | */ 991 | 992 | 993 | // 994 | // CellularInterpreterCheckNcpFailure 995 | // 996 | 997 | CellularInterpreterCheckNcpFailure::CellularInterpreterCheckNcpFailure() { 998 | } 999 | 1000 | CellularInterpreterCheckNcpFailure::~CellularInterpreterCheckNcpFailure() { 1001 | } 1002 | 1003 | void CellularInterpreterCheckNcpFailure::setup(CellularInterpreterCallback callback) { 1004 | // logMonitor.category = "hal"; // is ncp.client in 2.0.0 1005 | logMonitor.level = "ERROR"; 1006 | logMonitor.matchString = "No response from NCP"; 1007 | logMonitor.callback = [this, callback](long ts, const char *category, const char *level, const char *msg) { 1008 | _log.info("CellularInterpreterCheckNcpFailure callback called"); 1009 | if (noResponseCount >= 0) { 1010 | if (++noResponseCount >= 2) { 1011 | _log.info("checkNcpFailure detected"); 1012 | if (callback) { 1013 | callback(); 1014 | } 1015 | CellularInterpreter::getInstance()->blinkNotification(CellularInterpreter::BLINK_NCP_FAILURE, 3); 1016 | noResponseCount = -1; 1017 | } 1018 | } 1019 | }; 1020 | 1021 | CellularInterpreter::getInstance()->addLogMonitor(&logMonitor); 1022 | } 1023 | 1024 | // [static] 1025 | CellularInterpreterCheckNcpFailure *CellularInterpreterCheckNcpFailure::check(CellularInterpreterCallback callback) { 1026 | CellularInterpreterCheckNcpFailure *obj = new CellularInterpreterCheckNcpFailure(); 1027 | if (obj) { 1028 | obj->setup(callback); 1029 | } 1030 | return obj; 1031 | } 1032 | 1033 | -------------------------------------------------------------------------------- /src/CellularInterpreterRK.h: -------------------------------------------------------------------------------- 1 | #ifndef __CELLULARINTERPRETER_H 2 | #define __CELLULARINTERPRETER_H 3 | 4 | #include "Particle.h" 5 | 6 | #include 7 | #include 8 | 9 | #define TIMES_COLOR(n, c) (((n) << 24) | (c)) 10 | 11 | 12 | typedef std::function CellularInterpreterCallback; 13 | 14 | class CellularInterpreterParser { 15 | public: 16 | enum class RequestType : int { 17 | UNKNOWN_REQUEST = 0, 18 | SET_REQUEST, 19 | READ_REQUEST, 20 | TEST_REQUEST 21 | }; 22 | 23 | CellularInterpreterParser(); 24 | virtual ~CellularInterpreterParser(); 25 | 26 | void clear(); 27 | 28 | void parse(const char *command); 29 | 30 | /** 31 | * @brief Get the number of arguments 32 | * 33 | * Returns 1 for one argument. The index parameter is 0-based so the largest valid index 34 | * is getNumArgs() - 1. 35 | */ 36 | size_t getNumArgs() const { return args.size(); }; 37 | 38 | /** 39 | * @brief Returns the string argument by index 40 | * 41 | * @param index The 0-based index to get. 0 = first argument. Last argument is 42 | * is getNumArgs() - 1. 43 | * 44 | * @return The argument as a String. If the string argument is quoted, the double quotes 45 | * are removed. This also works if the item is an integer or hex, it just returns the 46 | * text representation in the argument response. 47 | */ 48 | String getArgString(size_t index) const; 49 | 50 | int getArgInt(size_t index) const; 51 | 52 | long getArgLongHex(size_t index) const; 53 | 54 | /** 55 | * @brief Returns the command part of the command line, if available 56 | * 57 | * For a send of an AT command, this will be the "AT+XXXX" part of "AT+XXXX=" with the parts being 58 | * the part after the "="". 59 | * 60 | * For a + response or a URC, this will be the "+XXXX" part of "+XXXX: " with the parts being the 61 | * part after the ": ". 62 | */ 63 | String getCommand() const { return command; }; 64 | 65 | RequestType getRequestType() const { return requestType; }; 66 | bool isSet() const { return requestType == RequestType::SET_REQUEST; }; 67 | bool isRead() const { return requestType == RequestType::READ_REQUEST; }; 68 | bool isTest() const { return requestType == RequestType::TEST_REQUEST; }; 69 | 70 | protected: 71 | RequestType requestType = RequestType::UNKNOWN_REQUEST; 72 | String command; 73 | std::vector args; 74 | }; 75 | 76 | 77 | typedef std::function CellularInterpreterLogCallback; 78 | 79 | class CellularInterpreterLogMonitor { 80 | public: 81 | String category; 82 | String level; 83 | String matchString; 84 | 85 | CellularInterpreterLogCallback callback; 86 | }; 87 | 88 | class CellularInterpreterModemMonitor; // Forward declaration 89 | 90 | typedef std::function CellularInterpreterModemCallback; 91 | 92 | /** 93 | * @brief Class to monitor the modem commands 94 | */ 95 | class CellularInterpreterModemMonitor { 96 | public: 97 | static const uint32_t REASON_OK = 0x00000001; 98 | static const uint32_t REASON_PLUS = 0x00000002; 99 | static const uint32_t REASON_ERROR = 0x00000004; 100 | static const uint32_t REASON_SEND = 0x00000008; 101 | static const uint32_t REASON_TIMEOUT = 0x00000010; 102 | static const uint32_t REASON_URC = 0x00000020; 103 | 104 | String command; 105 | uint32_t reasonFlags = 0; 106 | CellularInterpreterModemCallback callback; 107 | uint32_t timeout = 10000; 108 | uint64_t nextTimeout = 0; 109 | CellularInterpreterParser request; 110 | }; 111 | 112 | class CellularInterpreterCommand { 113 | public: 114 | String command; 115 | unsigned long startTime; 116 | bool gotPlus; 117 | bool complete; 118 | }; 119 | 120 | 121 | class CellularInterpreterBlinkPattern { 122 | public: 123 | CellularInterpreterBlinkPattern(); 124 | virtual ~CellularInterpreterBlinkPattern(); 125 | 126 | virtual void start() = 0; 127 | virtual bool run() = 0; 128 | 129 | virtual void callStart(); 130 | virtual bool callRun(); 131 | 132 | CellularInterpreterBlinkPattern &withRepeats(size_t num) { repeats = num; return *this; }; 133 | 134 | static void setColor(uint32_t color); 135 | 136 | protected: 137 | size_t repeats = 1; 138 | size_t curRepeat = 0; 139 | }; 140 | 141 | class CellularInterpreterBlinkPatternBlink : public CellularInterpreterBlinkPattern { 142 | public: 143 | CellularInterpreterBlinkPatternBlink(); 144 | virtual ~CellularInterpreterBlinkPatternBlink(); 145 | 146 | CellularInterpreterBlinkPatternBlink &withSlowBlink(uint32_t color, size_t blinks); 147 | 148 | virtual void start(); 149 | virtual bool run(); 150 | 151 | bool stateStart(); 152 | bool stateBeforeOff(); 153 | bool stateOn(); 154 | bool stateOff(); 155 | bool stateAfterOff(); 156 | 157 | protected: 158 | // Input parameters 159 | unsigned long beforeOffMs = 2000; 160 | unsigned long onPeriodMs = 250; 161 | unsigned long offPeriodMs = 750; 162 | size_t blinks = 1; 163 | unsigned long afterOffMs = 250; 164 | uint32_t onColor = RGB_COLOR_GREEN; 165 | uint32_t offColor = 0; 166 | 167 | // Current state 168 | std::function stateHandler = &CellularInterpreterBlinkPatternBlink::stateStart; 169 | size_t curBlink = 0; 170 | unsigned long stateTime = 0; 171 | }; 172 | 173 | 174 | class CellularInterpreterBlinkManager { 175 | public: 176 | CellularInterpreterBlinkManager(); 177 | virtual ~CellularInterpreterBlinkManager(); 178 | 179 | void setup(); 180 | 181 | void loop(); 182 | 183 | void addBlinkPattern(CellularInterpreterBlinkPattern *pat); 184 | 185 | static CellularInterpreterBlinkManager *getInstance() { return instance; }; 186 | 187 | protected: 188 | CellularInterpreterBlinkPattern *curPattern = 0; 189 | std::deque patternQueue; 190 | static CellularInterpreterBlinkManager *instance; 191 | }; 192 | 193 | class CellularInterpreterQueueEntry { 194 | public: 195 | enum class EntryType { 196 | UNKNOWN, 197 | LOG_ENTRY, 198 | MODEM_ENTRY, 199 | LOG_SETTINGS 200 | }; 201 | EntryType entryType = EntryType::UNKNOWN; 202 | long ts; 203 | String category; 204 | String level; 205 | String message; 206 | bool toModem; 207 | uint32_t andMask; // LOG_SETTINGS 208 | uint32_t orMask; // LOG_SETTINGS 209 | }; 210 | 211 | 212 | class CellularInterpreter : public StreamLogHandler, public Print { 213 | public: 214 | CellularInterpreter(); 215 | virtual ~CellularInterpreter(); 216 | 217 | void setup(); 218 | 219 | void loop(); 220 | 221 | void blinkNotification(uint32_t timesColor, size_t repeats = 1); 222 | 223 | void addLogMonitor(CellularInterpreterLogMonitor *mon); 224 | 225 | void addLogMonitor(const char *category, const char *level, const char *matchString, CellularInterpreterLogCallback callback); 226 | 227 | void addModemMonitor(CellularInterpreterModemMonitor *mon); 228 | 229 | void addModemMonitor(const char *cmdName, uint32_t reasonFlags, CellularInterpreterModemCallback callback, unsigned long timeout = 5000); 230 | 231 | /** 232 | * @brief Add a URC callback 233 | * 234 | * @param urc The URC. Just the text part ("CIEV") without the + or :. 235 | */ 236 | void addUrcHandler(const char *urc, CellularInterpreterModemCallback callback); 237 | 238 | void processLine(char *lineBuffer); 239 | 240 | 241 | void queueLogSettings(uint32_t andMask, uint32_t orMask); 242 | 243 | bool includeCommandInLog(const char *cmd) const; 244 | 245 | void logCommand(CellularInterpreterQueueEntry *entry); 246 | 247 | void processCommand(CellularInterpreterQueueEntry *entry); 248 | 249 | void callCommandMonitors(uint32_t reasonFlags, const char *command); 250 | 251 | void processTimeouts(); 252 | 253 | void processLog(CellularInterpreterQueueEntry *entry); 254 | 255 | 256 | /** 257 | * @brief Virtual override for the StreamLogHandler to write data to the log 258 | * 259 | * Declared in class Print, overridden here. 260 | */ 261 | virtual size_t write(uint8_t); 262 | 263 | void logOutput(uint8_t c); 264 | void logOutput(const char *s); 265 | 266 | bool logSettingsTraceEnabled() const { return (logSettings & LOG_TRACE) != 0; }; 267 | 268 | uint32_t getLogSettings() const { return logSettings; }; 269 | uint32_t updateLogSettings(uint32_t andMask, uint32_t orMask); 270 | 271 | static const uint32_t LOG_SERIAL = 0x00000001; 272 | static const uint32_t LOG_SERIAL1 = 0x00000002; 273 | static const uint32_t LOG_TRACE = 0x00000010; 274 | static const uint32_t LOG_VERBOSE = 0x00000020; 275 | static const uint32_t LOG_LITERAL = 0x00000040; 276 | 277 | 278 | static const size_t WRITE_BUF_SIZE = 100; 279 | static const size_t MAX_LINES = 30; 280 | 281 | static const uint32_t BLINK_NCP_FAILURE = TIMES_COLOR(5, RGB_COLOR_ORANGE); 282 | 283 | static bool isMatchingCommand(const char *test, const char *cmd); 284 | 285 | static String mapValueToString(const char *mapping, int value); 286 | 287 | static CellularInterpreter *getInstance() { return instance; }; 288 | 289 | 290 | protected: 291 | uint32_t logSettings = LOG_SERIAL | LOG_TRACE; 292 | char writeBuffer[WRITE_BUF_SIZE]; 293 | size_t writeOffset = 0; 294 | 295 | os_thread_t loopThread = 0; 296 | bool ignoreNextSend = false; 297 | bool ignoreNextOK = false; 298 | String lastCommand; 299 | 300 | std::deque queue; 301 | 302 | 303 | std::vector commandMonitors; 304 | std::vector logMonitors; 305 | static CellularInterpreter *instance; 306 | }; 307 | 308 | 309 | class CellularInterpreterCheckNcpFailure { 310 | public: 311 | CellularInterpreterCheckNcpFailure(); 312 | virtual ~CellularInterpreterCheckNcpFailure(); 313 | 314 | void setup(CellularInterpreterCallback callback); 315 | 316 | static CellularInterpreterCheckNcpFailure *check(CellularInterpreterCallback callback = 0); 317 | 318 | protected: 319 | CellularInterpreterLogMonitor logMonitor; 320 | int noResponseCount = 0; 321 | }; 322 | 323 | 324 | 325 | #endif /* __CELLULARINTERPRETER_H */ 326 | -------------------------------------------------------------------------------- /src/CloudDebug.cpp: -------------------------------------------------------------------------------- 1 | #include "Particle.h" 2 | 3 | SYSTEM_THREAD(ENABLED); 4 | SYSTEM_MODE(SEMI_AUTOMATIC); 5 | 6 | // Log handler is created dynamically below 7 | 8 | #include "CloudDebug.h" 9 | #include "CellularInterpreterRK.h" 10 | #include "sourcever.h" 11 | 12 | #include 13 | 14 | void buttonHandler(); 15 | void outOfMemoryHandler(system_event_t event, int param); 16 | void stateStart(); 17 | void stateStartTest(); 18 | 19 | #if SYSTEM_VERSION < 0x01020100 20 | #error Device OS 1.2.1 or later is required 21 | #endif 22 | 23 | SerialCommandEditor<1000, 256, 16> commandParser; 24 | 25 | const std::chrono::milliseconds autoStartScanTime = 15s; 26 | 27 | bool showTrace = true; 28 | bool traceManuallySet = false; 29 | bool buttonClicked = false; 30 | bool startTest = false; 31 | StateHandler stateHandler = stateStart; 32 | unsigned long stateTime; 33 | unsigned long lastConnectReport = 0; 34 | unsigned long cloudConnectTime = 0; 35 | std::vector traceStack; 36 | CellularInterpreter cellularInterpreter; 37 | int memoryRequestFailure = -1; 38 | 39 | void subscriptionHandler(const char *eventName, const char *data); 40 | 41 | void setup() { 42 | Particle.subscribe("particle/device/", subscriptionHandler, MY_DEVICES); 43 | System.on(button_click, buttonHandler); 44 | System.on(out_of_memory, outOfMemoryHandler); 45 | 46 | // This application also works like Tinker, allowing it to be controlled from 47 | // the Particle mobile app. This function initializes the Particle.functions. 48 | tinkerSetup(); 49 | 50 | cellularInterpreter.setup(); 51 | 52 | networkSetup(); 53 | 54 | // Configuration of prompt and welcome message 55 | commandParser 56 | .withPrompt("> ") 57 | .withWelcome("Particle Cloud Debugging Tool\nEnter 'help' command to list available commands"); 58 | 59 | commandParser.addCommandHandler("test", "start running tests", [](SerialCommandParserBase *) { 60 | startTest = true; 61 | commandParser.printMessage("starting tests..."); 62 | }); 63 | 64 | #if HAL_PLATFORM_CLOUD_UDP 65 | commandParser.addCommandHandler("keepAlive", "Set Particle cloud keepAlive ", [](SerialCommandParserBase *) { 66 | CommandParsingState *cps = commandParser.getParsingState(); 67 | if (cps->getNumExtraArgs() == 1) { 68 | int keepAlive = cps->getArgInt(0); 69 | if (keepAlive > 0) { 70 | commandParser.printMessageNoPrompt("keepAlive set to %d", keepAlive); 71 | Particle.keepAlive(keepAlive); 72 | } 73 | else { 74 | commandParser.printMessageNoPrompt("keepAlive cannot be 0"); 75 | } 76 | } 77 | else { 78 | commandParser.printMessageNoPrompt("missing keepAlive value in minutes"); 79 | commandParser.printHelpForCommand(cps->getCommandHandlerInfo()); 80 | } 81 | commandParser.printMessagePrompt(); 82 | }); 83 | #endif 84 | 85 | commandParser.addCommandHandler("safemode", "enter safe mode (blinking magenta)", [](SerialCommandParserBase *) { 86 | System.enterSafeMode(); 87 | commandParser.printMessagePrompt(); 88 | }); 89 | commandParser.addCommandHandler("dfu", "enter DFU (blinking yellow)", [](SerialCommandParserBase *) { 90 | System.dfu(); 91 | commandParser.printMessagePrompt(); 92 | }); 93 | 94 | commandParser.addCommandHandler("cloud", "Particle cloud connect or disconnect", [](SerialCommandParserBase *) { 95 | CommandParsingState *cps = commandParser.getParsingState(); 96 | if (cps->getByShortOpt('c')) { 97 | commandParser.printMessageNoPrompt("connecting..."); 98 | Particle.connect(); 99 | } 100 | else 101 | if (cps->getByShortOpt('d')) { 102 | commandParser.printMessageNoPrompt("disconnecting..."); 103 | Particle.disconnect(); 104 | } 105 | else { 106 | commandParser.printMessageNoPrompt("missing -c or -d option connected=%d", Particle.connected()); 107 | commandParser.printHelpForCommand(cps->getCommandHandlerInfo()); 108 | } 109 | commandParser.printMessagePrompt(); 110 | }) 111 | .addCommandOption('c', "connect", "connect using using Particle.connect()") 112 | .addCommandOption('d', "disconnect", "disconnect using Particle.disconnect()"); 113 | 114 | commandParser.addCommandHandler("trace", "enable or disable trace logging", [](SerialCommandParserBase *) { 115 | CommandParsingState *cps = commandParser.getParsingState(); 116 | 117 | if (cps->getByShortOpt('e')) { 118 | commandParser.printMessageNoPrompt("enabling trace mode"); 119 | showTrace = true; 120 | } 121 | else 122 | if (cps->getByShortOpt('d')) { 123 | commandParser.printMessageNoPrompt("disabling trace mode"); 124 | showTrace = false; 125 | } 126 | else { 127 | // Toggle with no options 128 | showTrace = !showTrace; 129 | commandParser.printMessageNoPrompt("trace mode %s", showTrace ? "enabled" : "disabled"); 130 | } 131 | traceManuallySet = true; 132 | 133 | setTraceLogging(showTrace); 134 | commandParser.printMessagePrompt(); 135 | }) 136 | .addCommandOption('e', "enable", "enable trace logging") 137 | .addCommandOption('d', "disable", "disable trace logging"); 138 | 139 | commandParser.addCommandHandler("literal", "enable or disable literal (non-filtered) logging", [](SerialCommandParserBase *) { 140 | CommandParsingState *cps = commandParser.getParsingState(); 141 | 142 | bool literal = (CellularInterpreter::getInstance()->getLogSettings() & CellularInterpreter::LOG_LITERAL) != 0; 143 | 144 | if (cps->getByShortOpt('e')) { 145 | commandParser.printMessageNoPrompt("enabling literal mode"); 146 | literal = true; 147 | } 148 | else 149 | if (cps->getByShortOpt('d')) { 150 | commandParser.printMessageNoPrompt("disabling literal mode"); 151 | literal = false; 152 | } 153 | else { 154 | // Toggle with no options 155 | literal = !literal; 156 | commandParser.printMessageNoPrompt("literal mode %s", literal ? "enabled" : "disabled"); 157 | } 158 | 159 | if (literal) { 160 | CellularInterpreter::getInstance()->updateLogSettings(~0, CellularInterpreter::LOG_LITERAL); 161 | } 162 | else { 163 | CellularInterpreter::getInstance()->updateLogSettings(~CellularInterpreter::LOG_LITERAL, 0); 164 | } 165 | 166 | commandParser.printMessagePrompt(); 167 | }) 168 | .addCommandOption('e', "enable", "enable trace logging") 169 | .addCommandOption('d', "disable", "disable trace logging"); 170 | 171 | 172 | #if 0 173 | // This does not currently work, not sure why 174 | commandParser.addCommandHandler("ping", "ping a host or IP address", [](SerialCommandParserBase *) { 175 | CommandParsingState *cps = commandParser.getParsingState(); 176 | if (cps->getParseSuccess()) { 177 | int tries = 3; 178 | IPAddress address; 179 | CommandOptionParsingState *cops; 180 | 181 | cops = cps->getByShortOpt('t'); 182 | if (cops) { 183 | tries = cops->getArgInt(0); 184 | if (tries < 1) { 185 | tries = 1; 186 | } 187 | } 188 | 189 | if (cps->getNumExtraArgs() == 1) { 190 | const char *hostOrIP = cps->getArgString(0); 191 | if (parseIP(hostOrIP, address, true)) { 192 | #if Wiring_WiFi 193 | int res = inet_ping(&address.raw(), WiFi, tries, NULL); 194 | #else 195 | int res = inet_ping(&address.raw(), Cellular, tries, NULL); 196 | #endif 197 | if (res > 0) { 198 | commandParser.printMessageNoPrompt("Ping %s succeeded!", address.toString().c_str()); 199 | } 200 | else { 201 | commandParser.printMessageNoPrompt("Unable to ping %s", address.toString().c_str()); 202 | } 203 | } 204 | } 205 | else { 206 | commandParser.printMessageNoPrompt("hostname or IP address required"); 207 | commandParser.printHelpForCommand(cps->getCommandHandlerInfo()); 208 | } 209 | } 210 | else { 211 | commandParser.printMessageNoPrompt("invalid options"); 212 | commandParser.printHelpForCommand(cps->getCommandHandlerInfo()); 213 | } 214 | 215 | commandParser.printMessagePrompt(); 216 | }) 217 | .addCommandOption('t', "tries", "number of tries"); 218 | #endif 219 | 220 | // Connect to Serial and start running 221 | commandParser 222 | .withSerial(&Serial) 223 | .setup(); 224 | 225 | stateTime = millis(); 226 | } 227 | 228 | void loop() { 229 | networkLoop(); 230 | 231 | cellularInterpreter.loop(); 232 | 233 | commandParser.loop(); 234 | 235 | if (stateHandler) { 236 | stateHandler(); 237 | } 238 | if (buttonClicked) { 239 | buttonClicked = false; 240 | stateHandler = stateButtonTest; 241 | } 242 | 243 | if (memoryRequestFailure > 0) { 244 | Log.error("Out of memory error: %d bytes request failed", memoryRequestFailure); 245 | memoryRequestFailure = -1; 246 | } 247 | } 248 | 249 | 250 | void buttonHandler() { 251 | buttonClicked = true; 252 | } 253 | 254 | void outOfMemoryHandler(system_event_t event, int param) { 255 | memoryRequestFailure = param; 256 | } 257 | 258 | void stateStart() { 259 | if (millis() - stateTime >= autoStartScanTime.count() && (commandParser.getBuffer()[0] == 0)) { 260 | // Automatically start running tests unless commands are being typed 261 | startTest = true; 262 | } 263 | 264 | if (startTest) { 265 | stateHandler = stateStartTest; 266 | } 267 | } 268 | 269 | /* 270 | static uint16_t convertCurrentLimit(byte b) { 271 | uint16_t result = 128; 272 | 273 | if (b & 0x1) result += 128; 274 | if (b & 0x2) result += 256; 275 | if (b & 0x4) result += 512; 276 | if (b & 0x8) result += 1024; 277 | 278 | return result; 279 | } 280 | */ 281 | 282 | void stateStartTest() { 283 | startTest = false; 284 | 285 | Log.info("Starting Tests!"); 286 | 287 | { 288 | String platform; 289 | 290 | switch(PLATFORM_ID) { 291 | case PLATFORM_GCC: 292 | platform = "gcc"; 293 | break; 294 | 295 | #ifdef PLATFORM_PHOTON_PRODUCTION 296 | case PLATFORM_PHOTON_PRODUCTION: 297 | platform = "Photon"; 298 | break; 299 | #endif 300 | 301 | #ifdef PLATFORM_P1 302 | case PLATFORM_P1: 303 | platform = "P1"; 304 | break; 305 | #endif 306 | 307 | #ifdef PLATFORM_ELECTRON_PRODUCTION 308 | case PLATFORM_ELECTRON_PRODUCTION: 309 | platform = "Electron/E Series"; 310 | break; 311 | #endif 312 | 313 | case PLATFORM_ARGON: 314 | platform = "Argon"; 315 | break; 316 | 317 | case PLATFORM_BORON: 318 | platform = "Boron"; 319 | break; 320 | 321 | case PLATFORM_ASOM: 322 | platform = "A SoM"; 323 | break; 324 | 325 | case PLATFORM_BSOM: 326 | platform = "B SoM"; 327 | break; 328 | 329 | #ifdef PLATFORM_B5SOM 330 | case PLATFORM_B5SOM: 331 | platform = "B5 SoM"; 332 | break; 333 | #endif 334 | 335 | #ifdef PLATFORM_TRACKER 336 | case PLATFORM_TRACKER: 337 | platform = "Tracker"; 338 | break; 339 | #endif 340 | 341 | #ifdef PLATFORM_P2 342 | case PLATFORM_P2: 343 | platform = "P2"; 344 | break; 345 | #endif 346 | 347 | default: 348 | platform = String::format("unknown platform %d", PLATFORM_ID); 349 | break; 350 | } 351 | 352 | Log.info("Platform: %s", platform.c_str()); 353 | } 354 | 355 | { 356 | uint8_t a, b, c, d; 357 | 358 | a = (uint8_t) (SYSTEM_VERSION >> 24); 359 | b = (uint8_t) (SYSTEM_VERSION >> 16); 360 | c = (uint8_t) (SYSTEM_VERSION >> 8); 361 | d = (uint8_t) (SYSTEM_VERSION); 362 | 363 | const char *relType = 0; 364 | switch(d & 0xc0) { 365 | case 0x80: 366 | relType = "rc"; 367 | d &= 0x3f; 368 | break; 369 | 370 | case 0x40: 371 | relType = "b"; 372 | d &= 0x3f; 373 | break; 374 | 375 | case 0x00: 376 | if ((a == 0) || ((a == 1) && (b < 2))) { 377 | // Prior to 1.2.0, all were RC if a was != 0 378 | relType = "rc"; 379 | } 380 | else { 381 | relType = "a"; 382 | } 383 | break; 384 | 385 | default: 386 | break; 387 | } 388 | 389 | if (relType) { 390 | Log.info("Binary compiled for: %d.%d.%d-%s.%d", a, b, c, relType, d); 391 | } 392 | else { 393 | Log.info("Binary compiled for: %d.%d.%d", a, b, c); 394 | } 395 | 396 | Log.info("Cloud Debug Release %d.%d.%d", a, b, SOURCEVER); 397 | 398 | Log.info("System version: %s", System.version().c_str()); 399 | 400 | Log.info("Device ID: %s", System.deviceID().c_str()); 401 | } 402 | 403 | runPowerReport(); 404 | 405 | checkEthernet(); 406 | 407 | stateHandler = stateStartNetworkTest; 408 | } 409 | 410 | void stateCloudWait() { 411 | if (Particle.connected()) { 412 | stateHandler = stateCloudReport; 413 | return; 414 | } 415 | 416 | if (millis() - lastConnectReport >= 10000) { 417 | lastConnectReport = millis(); 418 | Log.info("Still trying to connect to the cloud %s", elapsedString((millis() - stateTime) / 1000).c_str()); 419 | runPowerReport(); 420 | } 421 | } 422 | 423 | void stateCloudReport() { 424 | Log.info("Successfully connected to the Particle cloud in %s", elapsedString((millis() - stateTime) / 1000).c_str()); 425 | stateHandler = stateCloudConnected; 426 | cloudConnectTime = millis(); 427 | } 428 | 429 | void stateCloudConnected() { 430 | if (!Particle.connected()) { 431 | Log.info("Lost cloud connection after %s", elapsedString((millis() - cloudConnectTime) / 1000).c_str()); 432 | commandParser.printMessagePrompt(); 433 | lastConnectReport = millis(); 434 | stateHandler = stateCloudWait; 435 | return; 436 | } 437 | 438 | static unsigned long lastReport10s = 0; 439 | if (millis() - lastReport10s >= 10000) { 440 | lastReport10s = millis(); 441 | 442 | Log.info("Cloud connected for %s", elapsedString((millis() - cloudConnectTime) / 1000).c_str()); 443 | runReport10s(); 444 | runPowerReport(); 445 | 446 | Log.info("Free Memory: %lu", System.freeMemory()); 447 | 448 | commandParser.printMessagePrompt(); 449 | } 450 | 451 | static bool reportedTime = false; 452 | if (Time.isValid() && !reportedTime) { 453 | reportedTime = true; 454 | Log.info("Time: %s", Time.format().c_str()); 455 | commandParser.printMessagePrompt(); 456 | } 457 | 458 | static bool requestedName = false; 459 | if (!requestedName && millis() - cloudConnectTime >= 10000) { 460 | // 10 seconds after connecting to the cloud request the device name and IP address 461 | requestedName = true; 462 | Log.info("Requesting device name and IP address..."); 463 | Particle.publish("particle/device/name", "", PRIVATE); 464 | Particle.publish("particle/device/ip", "", PRIVATE); 465 | } 466 | 467 | } 468 | 469 | void stateIdle() { 470 | 471 | } 472 | 473 | void setTraceLogging(bool trace) { 474 | // Log.info("%s trace logging", (trace ? "enabling" : "disabling")); 475 | 476 | if (trace) { 477 | CellularInterpreter::getInstance()->queueLogSettings(~0, CellularInterpreter::LOG_TRACE); 478 | } 479 | else { 480 | CellularInterpreter::getInstance()->queueLogSettings(~CellularInterpreter::LOG_TRACE, 0); 481 | } 482 | } 483 | 484 | void pushTraceLogging(bool trace) { 485 | bool curTrace = (CellularInterpreter::getInstance()->getLogSettings() & CellularInterpreter::LOG_TRACE) != 0; 486 | 487 | traceStack.push_back(curTrace); 488 | setTraceLogging(trace); 489 | } 490 | 491 | void popTraceLogging() { 492 | setTraceLogging(traceStack.back()); 493 | traceStack.pop_back(); 494 | } 495 | 496 | 497 | #if PLATFORM_ID == PLATFORM_ARGON 498 | static bool hasVUSB() { 499 | uint32_t *pReg = (uint32_t *)0x40000438; // USBREGSTATUS 500 | 501 | return (*pReg & 1) != 0; 502 | } 503 | #endif /* PLATFORM_ID == PLATFORM_ARGON */ 504 | 505 | void runPowerReport() { 506 | 507 | #if HAL_PLATFORM_POWER_MANAGEMENT 508 | { 509 | String powerSourceStr; 510 | 511 | int powerSource = System.powerSource(); 512 | switch(powerSource) { 513 | case POWER_SOURCE_UNKNOWN: 514 | powerSourceStr = "unknown"; 515 | break; 516 | 517 | case POWER_SOURCE_VIN: 518 | powerSourceStr = "VIN"; 519 | break; 520 | 521 | case POWER_SOURCE_USB_HOST: 522 | powerSourceStr = "USB Host"; 523 | break; 524 | 525 | case POWER_SOURCE_USB_ADAPTER: 526 | powerSourceStr = "USB Adapter"; 527 | break; 528 | 529 | case POWER_SOURCE_USB_OTG: 530 | powerSourceStr = "USB OTG"; 531 | break; 532 | 533 | case POWER_SOURCE_BATTERY: 534 | powerSourceStr = "Battery"; 535 | break; 536 | 537 | default: 538 | powerSourceStr = String::format("powerSource %d", powerSource); 539 | break; 540 | } 541 | Log.info("Power source: %s", powerSourceStr.c_str()); 542 | } 543 | { 544 | String batteryStateStr; 545 | 546 | int batteryState = System.batteryState(); 547 | switch(batteryState) { 548 | case BATTERY_STATE_UNKNOWN: 549 | batteryStateStr = "unknown"; 550 | break; 551 | 552 | case BATTERY_STATE_NOT_CHARGING: 553 | batteryStateStr = "not charging"; 554 | break; 555 | 556 | case BATTERY_STATE_CHARGING: 557 | batteryStateStr = "charging"; 558 | break; 559 | 560 | case BATTERY_STATE_CHARGED: 561 | batteryStateStr = "charged"; 562 | break; 563 | 564 | case BATTERY_STATE_DISCHARGING: 565 | batteryStateStr = "discharging"; 566 | break; 567 | 568 | case BATTERY_STATE_FAULT: 569 | batteryStateStr = "fault"; 570 | break; 571 | 572 | case BATTERY_STATE_DISCONNECTED: 573 | batteryStateStr = "disconnected"; 574 | break; 575 | 576 | default: 577 | batteryStateStr = String::format("batteryState %d", batteryState); 578 | break; 579 | } 580 | Log.info("Battery state: %s, SoC: %.2f", batteryStateStr.c_str(), System.batteryCharge()); 581 | } 582 | #endif /* HAL_PLATFORM_POWER_MANAGEMENT */ 583 | 584 | #if PLATFORM_ID == PLATFORM_ARGON 585 | float voltage = analogRead(BATT) * 0.0011224; 586 | 587 | Log.info("USB power: %s, Battery Volage: %.2f", (hasVUSB() ? "true" : "false"), voltage); 588 | #endif /* PLATFORM_ID == PLATFORM_ARGON */ 589 | 590 | } 591 | 592 | 593 | String elapsedString(int sec) { 594 | 595 | int hh = sec / 3600; 596 | int mm = (sec / 60) % 60; 597 | int ss = sec % 60; 598 | 599 | if (sec < 3600) { 600 | // Less than an hour 601 | return String::format("%02d:%02d", mm, ss); 602 | } 603 | else { 604 | // More than an hour 605 | return String::format("%d:%02d:%02d", hh, mm, ss); 606 | } 607 | } 608 | 609 | 610 | bool parseIP(const char *str, IPAddress &addr, bool resolveIfNecessary) { 611 | bool valid = false; 612 | int value[4]; 613 | 614 | if (sscanf(str, "%d.%d.%d.%d", &value[0], &value[1], &value[2], &value[3]) == 4) { 615 | valid = true; 616 | for(size_t ii = 0; ii < 4; ii++) { 617 | if (value[ii] < 0 || value[ii] > 255) { 618 | valid = false; 619 | break; 620 | } 621 | } 622 | if (valid) { 623 | addr = IPAddress((uint8_t)value[0], (uint8_t)value[1], (uint8_t)value[2], (uint8_t)value[3]); 624 | } 625 | } 626 | else 627 | if (resolveIfNecessary) { 628 | HAL_IPAddress halAddress; 629 | 630 | // Not an IP address, try looking up DNS 631 | if (inet_gethostbyname(str, strlen(str), &halAddress, 0, NULL) == 0) { 632 | valid = true; 633 | addr = halAddress; 634 | } 635 | } 636 | 637 | return valid; 638 | } 639 | 640 | 641 | void subscriptionHandler(const char *eventName, const char *data) { 642 | const char *lastPart = strrchr(eventName, '/'); 643 | if (!lastPart) { 644 | return; 645 | } 646 | lastPart++; 647 | 648 | if (strcmp(lastPart, "ip") == 0) { 649 | Log.info("Public IP address: %s", data); 650 | } 651 | else 652 | if (strcmp(lastPart, "name") == 0) { 653 | Log.info("Device name: %s", data); 654 | } 655 | commandParser.printMessagePrompt(); 656 | } -------------------------------------------------------------------------------- /src/CloudDebug.h: -------------------------------------------------------------------------------- 1 | #ifndef __CLOUDDEBUG_H 2 | #define __CLOUDDEBUG_H 3 | 4 | #include "CellularHelper.h" 5 | #include "SerialCommandParserRK.h" 6 | 7 | // CloudDebug.h 8 | typedef void (*StateHandler)(); 9 | 10 | extern bool showTrace; 11 | extern bool traceManuallySet; 12 | extern bool buttonClicked; 13 | extern bool startTest; 14 | extern unsigned long stateTime; 15 | extern unsigned long lastConnectReport; 16 | extern StateHandler stateHandler; 17 | extern SerialCommandEditor<1000, 256, 16> commandParser; 18 | 19 | // Tinker.cpp 20 | void tinkerSetup(); 21 | 22 | // CloudDebug.cpp 23 | void stateCloudReport(); 24 | void stateCloudWait(); 25 | void stateCloudConnected(); 26 | void stateIdle(); 27 | void setTraceLogging(bool trace); 28 | void pushTraceLogging(bool trace); 29 | void popTraceLogging(); 30 | String elapsedString(int sec); 31 | void runPowerReport(); 32 | bool parseIP(const char *str, IPAddress &addr, bool resolveIfNecessary); 33 | 34 | // CloudDebugCellular.cpp or CloudDebugWiFi.cpp 35 | void networkSetup(); 36 | void networkLoop(); 37 | void stateStartNetworkTest(); 38 | void stateButtonTest(); 39 | void runReport10s(); 40 | 41 | // CloudDebugEthernet.cpp 42 | void checkEthernet(); 43 | 44 | #endif /* __CLOUDDEBUG_H */ 45 | -------------------------------------------------------------------------------- /src/CloudDebugCellular.cpp: -------------------------------------------------------------------------------- 1 | #include "CloudDebug.h" 2 | 3 | 4 | #if Wiring_Cellular 5 | 6 | // 7 | #include "CarrierLookupRK.h" 8 | #include "CellularHelp.h" 9 | 10 | void stateCellularWaitModemOn(); 11 | void stateCellularWait(); 12 | void stateCellularReport(); 13 | void stateCellularCloudWait(); 14 | void stateCellularCloudReport(); 15 | 16 | 17 | void reportNonModemInfo(); 18 | void reportModemInfo(); 19 | void runTowerTest(); 20 | 21 | // HAL_PLATFORM_GEN is only defined on 2.0.0 and later, so a little backward compatibility here 22 | #if !defined(HAL_PLATFORM_GEN) 23 | #if (PLATFORM_ID == PLATFORM_BORON) || (PLATFORM_ID == PLATFORM_BSOM) 24 | # define HAL_PLATFORM_GEN 3 25 | #elif defined(PLATFORM_B5SOM) && (PLATFORM_ID == PLATFORM_B5SOM) 26 | # define HAL_PLATFORM_GEN 3 27 | #elif defined(PLATFORM_TRACKER) && (PLATFORM_ID == PLATFORM_TRACKER) 28 | # define HAL_PLATFORM_GEN 3 29 | #else 30 | # define HAL_PLATFORM_GEN 2 31 | #endif 32 | #endif 33 | 34 | CellularHelperEnvironmentResponseStatic<32> envResp; 35 | static bool modemInfoReported = false; 36 | CellularHelp cellularHelp; 37 | 38 | void networkSetup() { 39 | 40 | cellular_on(NULL); 41 | 42 | 43 | // Add CellularHelp functions to CellularInterpreter 44 | cellularHelp.setup(); 45 | 46 | commandParser.addCommandHandler("tower", "report cell tower information", [](SerialCommandParserBase *) { 47 | runTowerTest(); 48 | commandParser.printMessagePrompt(); 49 | }); 50 | 51 | #if (HAL_PLATFORM_GEN >= 3) 52 | 53 | commandParser.addCommandHandler("setCredentials", "set APN for 3rd-party SIM (persistent)", [](SerialCommandParserBase *) { 54 | CommandParsingState *cps = commandParser.getParsingState(); 55 | 56 | String apn, user, pass; 57 | CommandOptionParsingState *cops; 58 | 59 | cops = cps->getByShortOpt('a'); 60 | if (cops) { 61 | apn = cops->getArgString(0); 62 | } 63 | 64 | cops = cps->getByShortOpt('u'); 65 | if (cops) { 66 | user = cops->getArgString(0); 67 | } 68 | 69 | cops = cps->getByShortOpt('p'); 70 | if (cops) { 71 | pass = cops->getArgString(0); 72 | } 73 | 74 | commandParser.printMessageNoPrompt("setCredentials apn=%s user=%s pass=%s (persistent)", apn.c_str(), user.c_str(), pass.c_str()); 75 | 76 | Cellular.setCredentials(apn, user, pass); 77 | 78 | commandParser.printMessagePrompt(); 79 | }) 80 | .addCommandOption('a', "apn", "specify APN", false, 1) 81 | .addCommandOption('u', "user", "specify username", false, 1) 82 | .addCommandOption('p', "pass", "specify password", false, 1); 83 | 84 | commandParser.addCommandHandler("clearCredentials", "clear APN for 3rd-party SIM (persistent)", [](SerialCommandParserBase *) { 85 | commandParser.printMessageNoPrompt("clearCredentials (persistent)"); 86 | Cellular.clearCredentials(); 87 | 88 | commandParser.printMessagePrompt(); 89 | }); 90 | 91 | commandParser.addCommandHandler("setActiveSim", "set active SIM (persistent)", [](SerialCommandParserBase *) { 92 | CommandParsingState *cps = commandParser.getParsingState(); 93 | 94 | String apn, user, pass; 95 | 96 | if (cps->getByShortOpt('i')) { 97 | commandParser.printMessageNoPrompt("setActiveSim INTERNAL_SIM"); 98 | Cellular.setActiveSim(INTERNAL_SIM); 99 | } 100 | else 101 | if (cps->getByShortOpt('e')) { 102 | commandParser.printMessageNoPrompt("setActiveSim EXTERNAL_SIM"); 103 | Cellular.setActiveSim(EXTERNAL_SIM); 104 | } 105 | else { 106 | commandParser.printMessageNoPrompt("setActiveSim must specify -i or -e"); 107 | } 108 | 109 | commandParser.printMessagePrompt(); 110 | }) 111 | .addCommandOption('e', "external", "use external SIM", false) 112 | .addCommandOption('i', "internal", "use internal SIM", false); 113 | 114 | #else 115 | // Gen 2 (Electron / E Series) 116 | commandParser.addCommandHandler("setCredentials", "set APN for 3rd-party SIM (not persistent)", [](SerialCommandParserBase *) { 117 | CommandParsingState *cps = commandParser.getParsingState(); 118 | 119 | String apn, user, pass; 120 | CommandOptionParsingState *cops; 121 | 122 | cops = cps->getByShortOpt('a'); 123 | if (cops) { 124 | apn = cops->getArgString(0); 125 | } 126 | 127 | cops = cps->getByShortOpt('u'); 128 | if (cops) { 129 | user = cops->getArgString(0); 130 | } 131 | 132 | cops = cps->getByShortOpt('p'); 133 | if (cops) { 134 | pass = cops->getArgString(0); 135 | } 136 | 137 | commandParser.printMessageNoPrompt("cellular_credentials_set apn=%s user=%s pass=%s", apn.c_str(), user.c_str(), pass.c_str()); 138 | 139 | cellular_credentials_set(apn, user, pass, NULL); 140 | 141 | commandParser.printMessagePrompt(); 142 | }) 143 | .addCommandOption('a', "apn", "specify APN", false, 1) 144 | .addCommandOption('u', "user", "specify username", false, 1) 145 | .addCommandOption('p', "pass", "specify password", false, 1); 146 | 147 | #endif /* HAL_PLATFORM_GEN >= 3 */ 148 | 149 | commandParser.addCommandHandler("mnoprof", "Set Mobile Network Operator Profile (persistent)", [](SerialCommandParserBase *) { 150 | CommandParsingState *cps = commandParser.getParsingState(); 151 | if (cps->getNumExtraArgs() == 1) { 152 | int mnoprof = cps->getArgInt(0); 153 | commandParser.printMessageNoPrompt("mnoprof set to %d (persistent)", mnoprof); 154 | Cellular.command("AT+COPS=2\r\n"); 155 | Cellular.command("AT+UMNOPROF=%d\r\n", mnoprof); 156 | Cellular.command("AT+CFUN=15\r\n"); 157 | } 158 | else { 159 | commandParser.printMessageNoPrompt("missing mnoprof value"); 160 | commandParser.printHelpForCommand(cps->getCommandHandlerInfo()); 161 | } 162 | commandParser.printMessagePrompt(); 163 | }); 164 | 165 | commandParser.addCommandHandler("command", "Send a raw cellular command", [](SerialCommandParserBase *) { 166 | if (commandParser.getArgsCount() == 2) { 167 | commandParser.printMessageNoPrompt("Sending command %s", commandParser.getArgString(1)); 168 | 169 | pushTraceLogging(true); 170 | int res = Cellular.command("%s\r\n", commandParser.getArgString(1)); 171 | popTraceLogging(); 172 | if (res == RESP_OK) { 173 | commandParser.printMessageNoPrompt("Success! (result=%d)", res); 174 | } 175 | else { 176 | commandParser.printMessageNoPrompt("Error: result=%d", res); 177 | } 178 | } 179 | else { 180 | commandParser.printMessageNoPrompt("missing command to send"); 181 | commandParser.printHelpForCommand("command"); 182 | } 183 | commandParser.printMessagePrompt(); 184 | }).withRawArgs(); 185 | 186 | commandParser.addCommandHandler("cellular", "Cellular connect or disconnect", [](SerialCommandParserBase *) { 187 | CommandParsingState *cps = commandParser.getParsingState(); 188 | if (cps->getByShortOpt('c')) { 189 | Cellular.connect(); 190 | } 191 | else 192 | if (cps->getByShortOpt('d')) { 193 | Cellular.disconnect(); 194 | } 195 | else { 196 | commandParser.printMessageNoPrompt("missing -c or -d option ready=%d", Cellular.ready()); 197 | commandParser.printHelpForCommand(cps->getCommandHandlerInfo()); 198 | } 199 | commandParser.printMessagePrompt(); 200 | }) 201 | .addCommandOption('c', "connect", "connect using using Cellular.connect()") 202 | .addCommandOption('d', "disconnect", "disconnect using Cellular.disconnect()"); 203 | 204 | } 205 | 206 | void networkLoop() { 207 | cellularHelp.loop(); 208 | } 209 | 210 | void stateStartNetworkTest() { 211 | modemInfoReported = false; 212 | stateTime = millis(); 213 | stateHandler = stateCellularWaitModemOn; 214 | } 215 | 216 | void stateButtonTest() { 217 | Log.info("running tower scan"); 218 | 219 | runTowerTest(); 220 | 221 | stateTime = millis(); 222 | stateHandler = stateIdle; 223 | } 224 | 225 | void stateCellularWaitModemOn() { 226 | if (millis() - stateTime < 8000) { 227 | // Wait 8 seconds for modem to stabilize 228 | return; 229 | } 230 | 231 | pushTraceLogging(false); 232 | 233 | reportNonModemInfo(); 234 | reportModemInfo(); 235 | 236 | popTraceLogging(); 237 | 238 | 239 | Cellular.connect(); 240 | 241 | stateTime = millis(); 242 | stateHandler = stateCellularWait; 243 | } 244 | 245 | 246 | void stateCellularWait() { 247 | // This only reports if we haven't yet reported and only tries every 10 seconds. 248 | // This is done because if the modem needs to be reset, the initial check above will fail 249 | // to retrieve any data. 250 | reportModemInfo(); 251 | 252 | if (Cellular.ready()) { 253 | stateHandler = stateCellularReport; 254 | return; 255 | } 256 | 257 | if (millis() - lastConnectReport >= 10000) { 258 | lastConnectReport = millis(); 259 | Log.info("Still trying to connect to cellular %s", elapsedString((millis() - stateTime) / 1000).c_str()); 260 | runPowerReport(); 261 | } 262 | } 263 | 264 | void stateCellularReport() { 265 | Log.info("Connected to cellular in %s", elapsedString((millis() - stateTime) / 1000).c_str()); 266 | 267 | Log.info("Connecting to the Particle cloud..."); 268 | Particle.connect(); 269 | 270 | stateTime = lastConnectReport = millis(); 271 | stateHandler = stateCloudWait; 272 | } 273 | 274 | 275 | void runReport10s() { 276 | // Runs every 10 seconds in stateIdle 277 | static int runs = 0; 278 | 279 | // First two times, leave trace logging on 280 | // After that, only report the file results to avoid noise 281 | pushTraceLogging(++runs <= 2); 282 | 283 | if (Particle.connected()) { 284 | CellularGlobalIdentity cgi = {0}; 285 | cgi.size = sizeof(CellularGlobalIdentity); 286 | cgi.version = CGI_VERSION_LATEST; 287 | 288 | cellular_result_t res = cellular_global_identity(&cgi, NULL); 289 | if (res == SYSTEM_ERROR_NONE) { 290 | Log.info("Cellular Info: cid=%lu lac=%u mcc=%u mnc=%u", cgi.cell_id, cgi.location_area_code, cgi.mobile_country_code, cgi.mobile_network_code); 291 | 292 | String country = lookupCountry(cgi.mobile_country_code); 293 | String carrier = lookupMccMnc(cgi.mobile_country_code, cgi.mobile_network_code); 294 | 295 | Log.info("Carrier: %s Country: %s", carrier.c_str(), country.c_str()); 296 | } 297 | else { 298 | Log.info("cellular_global_identity failed %d", res); 299 | } 300 | 301 | CellularHelperNetworkInfo networkInfo; 302 | if (CellularHelper.getNetworkInfo(networkInfo)) { 303 | Log.info("Technology: %s, Band: %s", networkInfo.accessTechnology.c_str(), networkInfo.band.c_str()); 304 | } 305 | } 306 | 307 | if (Cellular.ready()) { 308 | String ratString; 309 | 310 | CellularSignal sig = Cellular.RSSI(); 311 | int rat = sig.getAccessTechnology(); 312 | switch(rat) { 313 | case NET_ACCESS_TECHNOLOGY_GSM: 314 | ratString = "2G"; 315 | break; 316 | 317 | case NET_ACCESS_TECHNOLOGY_EDGE: 318 | ratString = "EDGE"; 319 | break; 320 | 321 | case NET_ACCESS_TECHNOLOGY_UMTS: 322 | // case NET_ACCESS_TECHNOLOGY_UTRAN: These are the same constant values 323 | // case NET_ACCESS_TECHNOLOGY_WCDMA: 324 | ratString = "3G"; 325 | break; 326 | 327 | case NET_ACCESS_TECHNOLOGY_LTE: 328 | ratString = "LTE"; 329 | break; 330 | 331 | case NET_ACCESS_TECHNOLOGY_LTE_CAT_M1: 332 | ratString = "LTE Cat M1"; 333 | break; 334 | 335 | case NET_ACCESS_TECHNOLOGY_LTE_CAT_NB1: 336 | ratString = "LTE Cat NB1"; 337 | break; 338 | 339 | default: 340 | ratString = String::format("unknown rat %d", rat); 341 | break; 342 | } 343 | 344 | Log.info("Strength: %.1f, Quality: %.1f, RAT: %s", sig.getStrength(), sig.getQuality(), ratString.c_str()); 345 | } 346 | 347 | // In almost all cases, this will already have been done, this is just a safety check to fix ch70101 348 | reportModemInfo(); 349 | 350 | popTraceLogging(); 351 | } 352 | 353 | void reportNonModemInfo() { 354 | Log.info("deviceID: %s", System.deviceID().c_str()); 355 | 356 | //#if (defined(HAL_PLATFORM_PMIC_BQ24195) && HAL_PLATFORM_PMIC_BQ24195) 357 | { 358 | PMIC pmic(true); 359 | 360 | Log.info("PMIC inputVoltageLimit: %d mV", 361 | (int) pmic.getInputVoltageLimit()); 362 | 363 | Log.info("PMIC inputCurrentLimit: %d mA", 364 | (int) pmic.getInputCurrentLimit()); 365 | 366 | Log.info("PMIC minimumSystemVoltage: %d mV", 367 | (int) pmic.getMinimumSystemVoltage()); 368 | 369 | Log.info("PMIC chargeCurrentValue: %d mA", 370 | (int) pmic.getChargeCurrentValue()); 371 | 372 | // Pre-Charge/Termination Current Register 3 does not appear to be exposed 373 | 374 | Log.info("PMIC chargeVoltageValue: %d mV", 375 | (int) pmic.getChargeVoltageValue()); 376 | } 377 | 378 | 379 | } 380 | 381 | void reportModemInfo() { 382 | if (modemInfoReported) { 383 | return; 384 | 385 | } 386 | static unsigned long lastCheck = 0; 387 | if (millis() - lastCheck < 10000) { 388 | return; 389 | } 390 | lastCheck = millis(); 391 | 392 | String mfg = CellularHelper.getManufacturer().c_str(); 393 | if (mfg.length() == 0) { 394 | Log.info("modem is not yet responding"); 395 | return; 396 | } 397 | 398 | modemInfoReported = true; 399 | 400 | pushTraceLogging(false); 401 | 402 | Log.info("manufacturer: %s", mfg.c_str()); 403 | 404 | Log.info("model: %s", CellularHelper.getModel().c_str()); 405 | 406 | Log.info("firmware version: %s", CellularHelper.getFirmwareVersion().c_str()); 407 | 408 | Log.info("ordering code: %s", CellularHelper.getOrderingCode().c_str()); 409 | 410 | Log.info("IMEI: %s", CellularHelper.getIMEI().c_str()); 411 | 412 | Log.info("IMSI: %s", CellularHelper.getIMSI().c_str()); 413 | 414 | Log.info("ICCID: %s", CellularHelper.getICCID().c_str()); 415 | 416 | popTraceLogging(); 417 | 418 | } 419 | 420 | 421 | void printCellData(CellularHelperEnvironmentCellData *data) { 422 | const char *whichG = data->isUMTS ? "3G" : "2G"; 423 | 424 | // Log.info("mcc=%d mnc=%d", data->mcc, data->mnc); 425 | 426 | String operatorName = lookupMccMnc(data->mcc, data->mnc); 427 | 428 | Log.info("%s %s %s %d bars", whichG, operatorName.c_str(), data->getBandString().c_str(), data->getBars()); 429 | } 430 | 431 | 432 | void runTowerTest() { 433 | envResp.clear(); 434 | envResp.enableDebug = true; 435 | 436 | const char *model = CellularHelper.getModel().c_str(); 437 | if (strncmp(model, "SARA-R4", 7) == 0) { 438 | Log.info("Tower scan not available on LTE (SARA-R4)"); 439 | return; 440 | } 441 | if (CellularHelper.getManufacturer().equals("Quectel")) { 442 | Log.info("Tower scan not available on Quectel"); 443 | return; 444 | } 445 | 446 | Log.info("Looking up operators (this may take up to 3 minutes)..."); 447 | 448 | // Command may take up to 3 minutes to execute! 449 | envResp.resp = Cellular.command(CellularHelperClass::responseCallback, (void *)&envResp, 360000, "AT+COPS=5\r\n"); 450 | if (envResp.resp == RESP_OK) { 451 | envResp.logResponse(); 452 | } 453 | 454 | Log.info("Results:"); 455 | 456 | printCellData(&envResp.service); 457 | if (envResp.neighbors) { 458 | for(size_t ii = 0; ii < envResp.numNeighbors; ii++) { 459 | if (envResp.neighbors[ii].isValid(true /* ignoreCI */)) { 460 | printCellData(&envResp.neighbors[ii]); 461 | } 462 | } 463 | } 464 | } 465 | 466 | #endif /* WiringCellular */ 467 | 468 | -------------------------------------------------------------------------------- /src/CloudDebugEthernet.cpp: -------------------------------------------------------------------------------- 1 | #include "CloudDebug.h" 2 | 3 | void checkEthernet() { 4 | #if Wiring_Ethernet 5 | Log.info("This device could have Ethernet (is 3rd generation)"); 6 | 7 | if (System.featureEnabled(FEATURE_ETHERNET_DETECTION)) { 8 | Log.info("FEATURE_ETHERNET_DETECTION enabled"); 9 | 10 | uint8_t mac[6]; 11 | if (Ethernet.macAddress(mac) != 0) { 12 | Log.info("Ethernet adapter present"); 13 | 14 | Ethernet.connect(); 15 | waitFor(Ethernet.ready, 10000); 16 | 17 | if (Ethernet.ready()) { 18 | Log.info("Ethernet ready (has link and IP address %s)", Ethernet.localIP().toString().c_str()); 19 | } 20 | else { 21 | Log.info("Was unable to establish link or no DHCP"); 22 | } 23 | } 24 | else { 25 | // the macAddress function returns 0 if there is no Ethernet adapter present 26 | Log.info("Ethernet detection enabled, but no Ethernet present"); 27 | } 28 | 29 | } 30 | else { 31 | Log.info("FEATURE_ETHERNET_DETECTION not enabled, so Ethernet will not be used (even if present)"); 32 | } 33 | #else 34 | Log.info("No Ethernet (not Gen 3)"); 35 | #endif 36 | } 37 | -------------------------------------------------------------------------------- /src/CloudDebugWiFi.cpp: -------------------------------------------------------------------------------- 1 | #include "CloudDebug.h" 2 | 3 | #if Wiring_WiFi && !HAL_PLATFORM_WIFI_SCAN_ONLY 4 | 5 | void stateWiFiWait(); 6 | void stateWiFiReport(); 7 | void stateWiFiCloudWait(); 8 | void stateWiFiCloudReport(); 9 | 10 | void wiFiScanCallback(WiFiAccessPoint* wap, void* data); 11 | String securityString(int value); 12 | String cipherString(int value); 13 | 14 | void networkSetup() { 15 | // Running in semi-automatic mode, turn on WiFi before beginning 16 | WiFi.on(); 17 | 18 | // antenna 19 | commandParser.addCommandHandler("antenna", "select antenna (persistent)", [](SerialCommandParserBase *) { 20 | CommandParsingState *cps = commandParser.getParsingState(); 21 | WLanSelectAntenna_TypeDef ant = ANT_NONE; 22 | 23 | if (cps->getByShortOpt('i')) { 24 | commandParser.printMessageNoPrompt("selecting internal antenna"); 25 | ant = ANT_INTERNAL; 26 | } 27 | else 28 | if (cps->getByShortOpt('e')) { 29 | commandParser.printMessageNoPrompt("selecting external antenna"); 30 | ant = ANT_EXTERNAL; 31 | } 32 | else 33 | if (cps->getByShortOpt('a')) { 34 | commandParser.printMessageNoPrompt("selecting automatic antenna"); 35 | ant = ANT_AUTO; 36 | } 37 | else { 38 | commandParser.printMessageNoPrompt("missing or unknown command options"); 39 | commandParser.printHelpForCommand(cps->getCommandHandlerInfo()); 40 | } 41 | if (ant != ANT_NONE) { 42 | WiFi.selectAntenna(ant); 43 | } 44 | 45 | commandParser.printMessagePrompt(); 46 | }) 47 | .addCommandOption('a', "auto", "select antenna automatically") 48 | .addCommandOption('i', "internal", "select internal antenna (factory default") 49 | .addCommandOption('e', "external", "select external antenna"); 50 | 51 | commandParser.addCommandHandler("wifi", "Wi-Fi connect or disconnect", [](SerialCommandParserBase *) { 52 | CommandParsingState *cps = commandParser.getParsingState(); 53 | if (cps->getByShortOpt('c')) { 54 | WiFi.connect(); 55 | } 56 | else 57 | if (cps->getByShortOpt('d')) { 58 | WiFi.disconnect(); 59 | } 60 | else { 61 | commandParser.printMessageNoPrompt("missing -c or -d option ready=%d", WiFi.ready()); 62 | commandParser.printHelpForCommand(cps->getCommandHandlerInfo()); 63 | } 64 | commandParser.printMessagePrompt(); 65 | }) 66 | .addCommandOption('c', "connect", "connect using using WiFi.connect()") 67 | .addCommandOption('d', "disconnect", "disconnect using WiFi.disconnect()"); 68 | 69 | // 70 | commandParser.addCommandHandler("clearCredentials", "Clear Wi-Fi credentials (persistent)", [](SerialCommandParserBase *) { 71 | WiFi.clearCredentials(); 72 | commandParser.printMessage("WiFi.clearCredentials called"); 73 | }); 74 | 75 | // 76 | commandParser.addCommandHandler("setCredentials", "Set Wi-Fi credentials (persistent)", [](SerialCommandParserBase *) { 77 | CommandParsingState *cps = commandParser.getParsingState(); 78 | if (cps->getParseSuccess()) { 79 | CommandOptionParsingState *cops; 80 | String ssid, password; 81 | unsigned long authType = WPA2; 82 | 83 | cops = cps->getByShortOpt('s'); 84 | if (cops) { 85 | ssid = cops->getArgString(0); 86 | } 87 | else 88 | cops = cps->getByShortOpt('p'); 89 | if (cops) { 90 | password = cops->getArgString(0); 91 | } 92 | else 93 | cops = cps->getByShortOpt('t'); 94 | if (cops) { 95 | String authTypeStr = cops->getArgString(0); 96 | if (authTypeStr == "UNSEC") { 97 | authType = UNSEC; 98 | } 99 | else 100 | if (authTypeStr == "WEP") { 101 | authType = WEP; 102 | } 103 | else 104 | if (authTypeStr == "WPA") { 105 | authType = WPA; 106 | } 107 | else 108 | if (authTypeStr == "WPA2") { 109 | authType = WPA2; 110 | } 111 | } 112 | 113 | if (password.length() > 0) { 114 | // authType default is WPA2, this is what the Wiring API does if authType is not specified 115 | WiFi.setCredentials(ssid, password, authType); 116 | } 117 | else { 118 | // No password, only use SSID (also ignore auth) 119 | WiFi.setCredentials(ssid); 120 | } 121 | commandParser.printMessage("setCredentials ssid=%s password=%s authType=%lu", 122 | ssid.c_str(), password.c_str(), authType); 123 | } 124 | else { 125 | commandParser.printMessageNoPrompt("invalid options"); 126 | commandParser.printHelpForCommand(cps->getCommandHandlerInfo()); 127 | } 128 | }) 129 | .addCommandOption('s', "ssid", "SSID (required)", true, 1) 130 | .addCommandOption('p', "password", "Password (required except for open networks)", false, 1) 131 | .addCommandOption('t', "t", "Authentication type WEP, WPA, WPA2 (required if AP not available)", false, 1); 132 | 133 | 134 | #if defined(PLATFORM_PHOTON_PRODUCTION) && (PLATFORM_ID == PLATFORM_PHOTON_PRODUCTION || PLATFORM_ID == PLATFORM_P1) 135 | // 136 | commandParser.addCommandHandler("useDynamicIP", "Turn off static IP address mode (persistent)", [](SerialCommandParserBase *) { 137 | WiFi.useDynamicIP(); 138 | commandParser.printMessage("WiFi.useDynamicIP called"); 139 | }); 140 | 141 | // 142 | commandParser.addCommandHandler("setStaticIP", "Set static IP address mode (persistent)", [](SerialCommandParserBase *) { 143 | CommandParsingState *cps = commandParser.getParsingState(); 144 | if (cps->getParseSuccess()) { 145 | IPAddress address, subnet, gateway, dns; 146 | CommandOptionParsingState *cops; 147 | 148 | cops = cps->getByShortOpt('a'); 149 | if (cops) { 150 | parseIP(cops->getArgString(0), address, false); 151 | } 152 | cops = cps->getByShortOpt('s'); 153 | if (cops) { 154 | parseIP(cops->getArgString(0), subnet, false); 155 | } 156 | cops = cps->getByShortOpt('g'); 157 | if (cops) { 158 | parseIP(cops->getArgString(0), gateway, false); 159 | } 160 | cops = cps->getByShortOpt('d'); 161 | if (cops) { 162 | parseIP(cops->getArgString(0), dns, false); 163 | } 164 | 165 | if (address && subnet && gateway && dns) { 166 | WiFi.setStaticIP(address, subnet, gateway, dns); 167 | WiFi.useStaticIP(); 168 | commandParser.printMessage("Static IP address set address=%s subnet=%s gateway=%s dns=%s", 169 | address.toString().c_str(), subnet.toString().c_str(), gateway.toString().c_str(), dns.toString().c_str()); 170 | } 171 | else { 172 | commandParser.printMessageNoPrompt("invalid IP address(es)"); 173 | commandParser.printHelpForCommand(cps->getCommandHandlerInfo()); 174 | } 175 | } 176 | else { 177 | commandParser.printMessageNoPrompt("invalid options"); 178 | commandParser.printHelpForCommand(cps->getCommandHandlerInfo()); 179 | } 180 | }) 181 | .addCommandOption('a', "address", "IP address (required)", true, 1) 182 | .addCommandOption('s', "subnet", "subnet mask", true, 1) 183 | .addCommandOption('g', "gateway", "Gateway IP address", true, 1) 184 | .addCommandOption('d', "dns", "DNS server IP address", true, 1); 185 | 186 | #endif 187 | 188 | } 189 | 190 | void stateStartNetworkTest() { 191 | 192 | #if defined(PLATFORM_PHOTON_PRODUCTION) && ((PLATFORM_ID == PLATFORM_PHOTON_PRODUCTION) || (PLATFORM_ID == PLATFORM_P1)) 193 | { 194 | WLanSelectAntenna_TypeDef ant = wlan_get_antenna(NULL); 195 | 196 | String antStr; 197 | switch(ant) { 198 | case ANT_INTERNAL: 199 | antStr = "internal"; 200 | break; 201 | 202 | case ANT_EXTERNAL: 203 | antStr = "external"; 204 | break; 205 | 206 | case ANT_AUTO: 207 | antStr = "auto"; 208 | break; 209 | 210 | default: 211 | antStr = String::format("unknown antenna %d", (int) ant); 212 | break; 213 | } 214 | 215 | Log.info("Antenna: %s", antStr.c_str()); 216 | } 217 | { 218 | IPAddressSource antSource = wlan_get_ipaddress_source(NULL); 219 | 220 | String antSourceString; 221 | switch(antSource) { 222 | case STATIC_IP: 223 | antSourceString = "static"; 224 | break; 225 | 226 | case DYNAMIC_IP: 227 | antSourceString = "dynamic"; 228 | break; 229 | 230 | default: 231 | antSourceString = String::format("unknown %d", (int)antSource); 232 | break; 233 | } 234 | Log.info("IP Address Configuration: %s", antSourceString.c_str()); 235 | 236 | if (antSource == STATIC_IP) { 237 | IPConfig conf; 238 | conf.size = sizeof(conf); 239 | wlan_get_ipaddress(&conf, NULL); 240 | Log.info("Static ipAddr: %s", IPAddress(conf.nw.aucIP).toString().c_str()); 241 | Log.info(" subnetMask: %s", IPAddress(conf.nw.aucSubnetMask).toString().c_str()); 242 | Log.info(" gateway: %s", IPAddress(conf.nw.aucDefaultGateway).toString().c_str()); 243 | Log.info(" dns: %s", IPAddress(conf.nw.aucDNSServer).toString().c_str()); 244 | } 245 | } 246 | #endif 247 | 248 | // If WiFi has been configured, print out the configuration (does not include passwords) 249 | if (WiFi.hasCredentials()) { 250 | Log.info("Configured credentials:"); 251 | WiFiAccessPoint ap[5]; 252 | int found = WiFi.getCredentials(ap, 5); 253 | for(int ii = 0; ii < found; ii++) { 254 | Log.info(" ssid=%s security=%s cipher=%s", 255 | ap[ii].ssid, 256 | securityString(ap[ii].security).c_str(), 257 | cipherString(ap[ii].cipher).c_str()); 258 | } 259 | } 260 | 261 | Log.info("Available access points:"); 262 | 263 | WiFi.scan(wiFiScanCallback); 264 | 265 | 266 | Log.info("Connecting to Wi-Fi"); 267 | 268 | if (!traceManuallySet) { 269 | showTrace = true; 270 | setTraceLogging(true); 271 | } 272 | 273 | WiFi.connect(); 274 | 275 | lastConnectReport = millis(); 276 | stateTime = millis(); 277 | stateHandler = stateWiFiWait; 278 | 279 | } 280 | 281 | void networkLoop() { 282 | 283 | } 284 | 285 | void stateButtonTest() { 286 | 287 | } 288 | 289 | void stateWiFiWait() { 290 | if (WiFi.ready()) { 291 | stateHandler = stateWiFiReport; 292 | return; 293 | } 294 | 295 | if (millis() - lastConnectReport >= 10000) { 296 | lastConnectReport = millis(); 297 | Log.info("Still trying to connect to Wi-Fi %s", elapsedString((millis() - stateTime) / 1000).c_str()); 298 | } 299 | } 300 | 301 | void stateWiFiReport() { 302 | Log.info("Connected to Wi-Fi in %s", elapsedString((millis() - stateTime) / 1000).c_str()); 303 | { 304 | uint8_t mac[6]; 305 | 306 | WiFi.macAddress(mac); 307 | Log.info("MAC address: %02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); 308 | } 309 | Log.info("localIP: %s", WiFi.localIP().toString().c_str()); 310 | Log.info("subnetMask: %s", WiFi.subnetMask().toString().c_str()); 311 | 312 | IPAddress gatewayIP = WiFi.gatewayIP(); 313 | Log.info("gatewayIP: %s", gatewayIP.toString().c_str()); 314 | 315 | IPAddress dnsServerIP = WiFi.dnsServerIP(); 316 | Log.info("dnsServerIP: %s (often 0.0.0.0)", dnsServerIP.toString().c_str()); 317 | Log.info("dhcpServerIP: %s (often 0.0.0.0)", WiFi.dhcpServerIP().toString().c_str()); 318 | 319 | { 320 | byte bssid[6]; 321 | WiFi.BSSID(bssid); 322 | Log.info("BSSID: %02x:%02x:%02x:%02x:%02x:%02x", bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); 323 | } 324 | 325 | if (gatewayIP) { 326 | int count = WiFi.ping(gatewayIP, 1); 327 | Log.info("ping gateway=%d", count); 328 | } 329 | if (dnsServerIP) { 330 | int count = WiFi.ping(dnsServerIP, 1); 331 | Log.info("ping dnsServerIP=%d", count); 332 | } 333 | 334 | { 335 | IPAddress addr = IPAddress(8,8,8,8); 336 | int count = WiFi.ping(addr, 1); 337 | Log.info("ping addr %s=%d", addr.toString().c_str(), count); 338 | } 339 | 340 | #if defined(PLATFORM_PHOTON_PRODUCTION) && ((PLATFORM_ID == PLATFORM_PHOTON_PRODUCTION) || (PLATFORM_ID == PLATFORM_P1)) 341 | { 342 | IPAddress addr; 343 | 344 | for(size_t tries = 1; tries <= 3; tries++) { 345 | addr = WiFi.resolve("device.spark.io"); 346 | if (addr) { 347 | break; 348 | } 349 | Log.info("failed to get device.spark.io from DNS, try %d", tries); 350 | delay(2000); 351 | } 352 | Log.info("device.spark.io=%s", addr.toString().c_str()); 353 | 354 | if (addr) { 355 | // This will always fail 356 | // int count = WiFi.ping(addr, 1); 357 | // Log.info("ping addr %s=%d", addr.toString().c_str(), count); 358 | 359 | TCPClient client; 360 | if (client.connect(addr, 5683)) { 361 | Log.info("connected to device server CoAP (testing connection only)"); 362 | client.stop(); 363 | } 364 | else { 365 | Log.info("could not connect to device server by CoAP"); 366 | } 367 | } 368 | } 369 | #endif 370 | 371 | Log.info("Connecting to the Particle cloud..."); 372 | Particle.connect(); 373 | 374 | stateTime = lastConnectReport = millis(); 375 | stateHandler = stateCloudWait; 376 | 377 | } 378 | 379 | void runReport10s() { 380 | // Runs every 10 seconds in stateIdle 381 | 382 | WiFiSignal sig = WiFi.RSSI(); 383 | 384 | byte bssid[6]; 385 | WiFi.BSSID(bssid); 386 | 387 | Log.info("rssi=%.1f bssid=%02X:%02X:%02X:%02X:%02X:%02X", 388 | sig.getStrengthValue(), 389 | bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]); 390 | 391 | } 392 | 393 | String securityString(int value) { 394 | String result; 395 | 396 | switch(value) { 397 | case WLAN_SEC_UNSEC: 398 | result = "unsecured"; 399 | break; 400 | 401 | case WLAN_SEC_WEP: 402 | result = "wep"; 403 | break; 404 | 405 | case WLAN_SEC_WPA: 406 | result = "wpa"; 407 | break; 408 | 409 | case WLAN_SEC_WPA2: 410 | result = "wpa2"; 411 | break; 412 | 413 | case WLAN_SEC_WPA_ENTERPRISE: 414 | result = "wpa enterprise"; 415 | break; 416 | 417 | case WLAN_SEC_WPA2_ENTERPRISE: 418 | result = "wpa2 enterprise"; 419 | break; 420 | 421 | case WLAN_SEC_NOT_SET: 422 | result = "not set"; 423 | break; 424 | 425 | default: 426 | result = String::format("unknown security %d", value); 427 | break; 428 | } 429 | return result; 430 | } 431 | 432 | String cipherString(int value) { 433 | String result; 434 | 435 | switch(value) { 436 | case WLAN_CIPHER_NOT_SET: 437 | result = "not set"; 438 | break; 439 | 440 | case WLAN_CIPHER_AES: 441 | result = "AES"; 442 | break; 443 | 444 | case WLAN_CIPHER_TKIP: 445 | result = "TKIP"; 446 | break; 447 | 448 | case WLAN_CIPHER_AES_TKIP: 449 | result = "AES or TKIP"; 450 | break; 451 | 452 | default: 453 | result = String::format("unknown cipher %d", value); 454 | break; 455 | } 456 | 457 | return result; 458 | } 459 | 460 | void wiFiScanCallback(WiFiAccessPoint* wap, void* data) { 461 | 462 | 463 | Log.info(" ssid=%s security=%s channel=%d rssi=%d", 464 | wap->ssid, securityString(wap->security).c_str(), wap->channel, wap->rssi); 465 | 466 | } 467 | 468 | 469 | #endif /* Wiring_WiFi */ 470 | 471 | -------------------------------------------------------------------------------- /src/Tinker.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019 Particle Industries, Inc. All rights reserved. 3 | * 4 | * This library is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU Lesser General Public 6 | * License as published by the Free Software Foundation, either 7 | * version 3 of the License, or (at your option) any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | * Lesser General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Lesser General Public 15 | * License along with this library; if not, see . 16 | */ 17 | 18 | /* Includes ------------------------------------------------------------------*/ 19 | #include "application.h" 20 | #include 21 | 22 | struct PinMapping { 23 | const char* name; 24 | pin_t pin; 25 | }; 26 | 27 | #define PIN(p) {#p, p} 28 | 29 | const PinMapping g_pinmap[] = { 30 | // Gen 2 31 | #if defined(PLATFORM_ELECTRON_PRODUCTION) && (PLATFORM_ID > PLATFORM_GCC && PLATFORM_ID <= PLATFORM_ELECTRON_PRODUCTION) 32 | PIN(D0), PIN(D1), PIN(D2), PIN(D3), PIN(D4), PIN(D5), PIN(D6), PIN(D7), 33 | PIN(A0), PIN(A1), PIN(A2), PIN(A3), PIN(A4), PIN(A5), PIN(A6), PIN(A7), 34 | PIN(RX), PIN(TX), PIN(WKP), PIN(SS), PIN(SCK), PIN(MISO), PIN(MOSI), 35 | PIN(SDA), PIN(SCL), PIN(DAC1), PIN(DAC) 36 | 37 | # if PLATFORM_ID == PLATFORM_P1 38 | , 39 | PIN(P1S0),PIN(P1S1), PIN(P1S2), PIN(P1S3), PIN(P1S4), PIN(P1S5), PIN(P1S6), 40 | # endif // PLATFORM_ID == PLATFORM_P1 41 | 42 | # if defined(PLATFORM_ELECTRON_PRODUCTION) && PLATFORM_ID == PLATFORM_ELECTRON_PRODUCTION 43 | , 44 | PIN(B0), PIN(B1), PIN(B2), PIN(B3), PIN(B4), PIN(B5), PIN(C0), PIN(C1), 45 | PIN(C2), PIN(C3), PIN(C4), PIN(C5) 46 | # endif // PLATFORM_ID == PLATFORM_ELECTRON_PRODUCTION 47 | 48 | // Gen 3 49 | #elif HAL_PLATFORM_NRF52840 // (PLATFORM_ID > PLATFORM_GCC && PLATFORM_ID <= PLATFORM_ELECTRON_PRODUCTION) 50 | # if PLATFORM_ID == PLATFORM_TRACKER 51 | PIN(D0), PIN(D1), PIN(D2), PIN(D3), PIN(D4), PIN(D5), PIN(D6), PIN(D7), PIN(D8), PIN(D9) 52 | # elif PLATFORM_ID == PLATFORM_ESOMX 53 | PIN(D0), PIN(D1), PIN(D2), PIN(D5), PIN(TX), PIN(RX), PIN(A0), PIN(A1), 54 | PIN(A2), PIN(A3), PIN(A4), PIN(A5), PIN(A6), PIN(A7), PIN(B0), PIN(B1), 55 | PIN(B2), PIN(B3), PIN(C0), PIN(C1), PIN(C2), PIN(C3), PIN(C4), PIN(C5), 56 | PIN(SS), PIN(SCK), PIN(MISO), PIN(MOSI), PIN(SDA), PIN(SCL), PIN(CTS), 57 | PIN(RTS), PIN(SS1), PIN(SCK1), PIN(MISO1), PIN(MOSI1), 58 | # else // PLATFORM_ID == PLATFORM_ESOMX 59 | PIN(D0), PIN(D1), PIN(D2), PIN(D3), PIN(D4), PIN(D5), PIN(D6), PIN(D7), 60 | PIN(D8), PIN(D9), PIN(D10), PIN(D11), PIN(D12), PIN(D13), PIN(D14), PIN(D15), 61 | PIN(D16), PIN(D17), PIN(D18), PIN(D19), PIN(A0), PIN(A1), PIN(A2), PIN(A3), 62 | PIN(A4), PIN(A5), PIN(SCK), PIN(MISO), PIN(MOSI), PIN(SDA), PIN(SCL), PIN(TX), 63 | PIN(RX) 64 | # if PLATFORM_ID == PLATFORM_BSOM || PLATFORM_ID == PLATFORM_B5SOM || PLATFORM_ID == PLATFORM_ASOM 65 | , 66 | PIN(D20), PIN(D21), PIN(D22), PIN(D23), 67 | PIN(A6), PIN(A7) 68 | # if PLATFORM_ID == PLATFORM_ASOM 69 | , 70 | PIN(D24) 71 | # endif // PLATFORM_ID == PLATFORM_ASOM 72 | # endif // PLATFORM_ID == PLATFORM_BSOM || PLATFORM_ID == PLATFORM_B5SOM || PLATFORM_ID == PLATFORM_ASOM 73 | # endif // PLATFORM_ID == PLATFORM_TRACKER 74 | 75 | // P2 76 | #elif HAL_PLATFORM_RTL872X 77 | PIN(D0), PIN(D1), PIN(D2), PIN(D3), PIN(D4), PIN(D5), PIN(D6), PIN(D7), PIN(D8), PIN(D9), 78 | PIN(D10), PIN(D11), PIN(D12), PIN(D13), PIN(D14), PIN(D15), PIN(D16), PIN(D17), PIN(D18), 79 | PIN(D19), PIN(D20), PIN(D21), 80 | PIN(A0), PIN(A1), PIN(A2), PIN(A3), PIN(A4), PIN(A5), 81 | PIN(SS), PIN(SCK), PIN(MISO), PIN(MOSI), PIN(SS1), PIN(SCK1), PIN(MISO1), PIN(MOSI1), 82 | PIN(SDA), PIN(SCL), 83 | PIN(TX), PIN(RX), PIN(TX1), PIN(RX1), PIN(CTS1), PIN(RTS1), 84 | PIN(WKP) 85 | #else // HAL_PLATFORM_RTL872X 86 | 87 | # error Unsupported platform 88 | #endif 89 | }; 90 | 91 | const size_t g_pin_count = sizeof(g_pinmap) / sizeof(*g_pinmap); 92 | 93 | /* Function prototypes -------------------------------------------------------*/ 94 | int tinkerDigitalRead(String pin); 95 | int tinkerDigitalWrite(String command); 96 | int tinkerAnalogRead(String pin); 97 | int tinkerAnalogWrite(String command); 98 | 99 | #ifdef LOG_SERIAL1 100 | Serial1LogHandler g_logSerial1(115200, LOG_LEVEL_ALL); 101 | #endif // LOG_SERIAL1 102 | 103 | #ifdef LOG_SERIAL 104 | SerialLogHandler g_logSerial(LOG_LEVEL_ALL); 105 | #endif // LOG_SERIAL 106 | 107 | #if defined(HAL_PLATFORM_POWER_MANAGEMENT_OPTIONAL) && HAL_PLATFORM_POWER_MANAGEMENT_OPTIONAL 108 | // Enable runtime PMIC / FuelGauge detection 109 | STARTUP(System.enable(SYSTEM_FLAG_PM_DETECTION)); 110 | #endif // defined(HAL_PLATFORM_POWER_MANAGEMENT_OPTIONAL) && HAL_PLATFORM_POWER_MANAGEMENT_OPTIONAL 111 | 112 | // PIN_INVALID is not available on Gen2 platforms < 1.1.0 113 | #ifndef PIN_INVALID 114 | #define PIN_INVALID (0xff) 115 | #endif // PIN_INVALID 116 | 117 | pin_t lookupPinByName(const String& name) { 118 | for (unsigned i = 0; i < g_pin_count; i++) { 119 | const auto& entry = g_pinmap[i]; 120 | if (!strcmp(entry.name, name.c_str())) { 121 | return entry.pin; 122 | } 123 | } 124 | 125 | return PIN_INVALID; 126 | } 127 | 128 | /* This function is called once at start up ----------------------------------*/ 129 | void tinkerSetup() 130 | { 131 | //Setup the Tinker application here 132 | 133 | //Register all the Tinker functions 134 | Particle.function("digitalread", tinkerDigitalRead); 135 | Particle.function("digitalwrite", tinkerDigitalWrite); 136 | 137 | Particle.function("analogread", tinkerAnalogRead); 138 | Particle.function("analogwrite", tinkerAnalogWrite); 139 | } 140 | 141 | /******************************************************************************* 142 | * Function Name : tinkerDigitalRead 143 | * Description : Reads the digital value of a given pin 144 | * Input : Pin 145 | * Output : None. 146 | * Return : Value of the pin (0 or 1) in INT type 147 | Returns a negative number on failure 148 | *******************************************************************************/ 149 | int tinkerDigitalRead(String pinStr) 150 | { 151 | pin_t pin = lookupPinByName(pinStr); 152 | if (pin != PIN_INVALID) { 153 | pinMode(pin, INPUT_PULLDOWN); 154 | return digitalRead(pin); 155 | } 156 | return -1; 157 | } 158 | 159 | /******************************************************************************* 160 | * Function Name : tinkerDigitalWrite 161 | * Description : Sets the specified pin HIGH or LOW 162 | * Input : Pin and value 163 | * Output : None. 164 | * Return : 1 on success and a negative number on failure 165 | *******************************************************************************/ 166 | int tinkerDigitalWrite(String command) 167 | { 168 | int separatorIndex = -1; 169 | for (unsigned i = 0; i < command.length(); ++i) { 170 | const char c = command.charAt(i); 171 | if (!std::isalnum((unsigned char)c)) { 172 | separatorIndex = i; 173 | break; 174 | } 175 | } 176 | if (separatorIndex <= 0) { 177 | return -1; 178 | } 179 | String pinStr = command.substring(0, separatorIndex); 180 | String pinState = command.substring(separatorIndex + 1); 181 | 182 | uint8_t state; 183 | if (pinState == "HIGH") { 184 | state = HIGH; 185 | } else if (pinState == "LOW") { 186 | state = LOW; 187 | } else { 188 | return -2; 189 | } 190 | 191 | pin_t pin = lookupPinByName(pinStr); 192 | 193 | if (pin != PIN_INVALID) { 194 | pinMode(pin, OUTPUT); 195 | digitalWrite(pin, state); 196 | return 1; 197 | } 198 | 199 | return -1; 200 | } 201 | 202 | /******************************************************************************* 203 | * Function Name : tinkerAnalogRead 204 | * Description : Reads the analog value of a pin 205 | * Input : Pin 206 | * Output : None. 207 | * Return : Returns the analog value in INT type (0 to 4095) 208 | Returns a negative number on failure 209 | *******************************************************************************/ 210 | int tinkerAnalogRead(String pinStr) 211 | { 212 | pin_t pin = lookupPinByName(pinStr); 213 | if (pin != PIN_INVALID && HAL_Validate_Pin_Function(pin, PF_ADC) == PF_ADC) { 214 | return analogRead(pin); 215 | } 216 | return -1; 217 | } 218 | 219 | /******************************************************************************* 220 | * Function Name : tinkerAnalogWrite 221 | * Description : Writes an analog value (PWM) to the specified pin 222 | * Input : Pin and Value (0 to 255) 223 | * Output : None. 224 | * Return : 1 on success and a negative number on failure 225 | *******************************************************************************/ 226 | int tinkerAnalogWrite(String command) 227 | { 228 | int separatorIndex = -1; 229 | for (unsigned i = 0; i < command.length(); ++i) { 230 | const char c = command.charAt(i); 231 | if (!std::isalnum((unsigned char)c)) { 232 | separatorIndex = i; 233 | break; 234 | } 235 | } 236 | if (separatorIndex <= 0) { 237 | return -1; 238 | } 239 | String pinStr = command.substring(0, separatorIndex); 240 | String pinValue = command.substring(separatorIndex + 1); 241 | 242 | int value = pinValue.toInt(); 243 | if (value < 0 || value > 255) { 244 | return -2; 245 | } 246 | 247 | pin_t pin = lookupPinByName(pinStr); 248 | if (pin != PIN_INVALID && (HAL_Validate_Pin_Function(pin, PF_DAC) == PF_DAC || 249 | HAL_Validate_Pin_Function(pin, PF_TIMER) == PF_TIMER)) { 250 | pinMode(pin, OUTPUT); 251 | analogWrite(pin, value); 252 | return 1; 253 | } 254 | 255 | return -1; 256 | } 257 | -------------------------------------------------------------------------------- /src/sourcever.h: -------------------------------------------------------------------------------- 1 | const int SOURCEVER = 5; 2 | --------------------------------------------------------------------------------