├── img ├── SW-420_board.jpg ├── SW-420_sensor.jpg └── IMG_20210916_142948.jpg ├── .vscode ├── settings.json └── extensions.json ├── .gitignore ├── src ├── credentials.h ├── images.h ├── credentials.cpp ├── configuration.h ├── images.cpp └── main.cpp ├── workspace.code-workspace ├── test └── README ├── platformio.ini ├── lib ├── README └── Serial │ ├── HardwareSerial.h │ └── HardwareSerial.cpp ├── include └── README ├── .clang-format └── README.md /img/SW-420_board.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Max-Plastix/CubeCell-GPS-Helium-Mapper/HEAD/img/SW-420_board.jpg -------------------------------------------------------------------------------- /img/SW-420_sensor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Max-Plastix/CubeCell-GPS-Helium-Mapper/HEAD/img/SW-420_sensor.jpg -------------------------------------------------------------------------------- /img/IMG_20210916_142948.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Max-Plastix/CubeCell-GPS-Helium-Mapper/HEAD/img/IMG_20210916_142948.jpg -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.tcc": "cpp", 4 | "fstream": "cpp", 5 | "iosfwd": "cpp", 6 | "type_traits": "cpp", 7 | "cstdio": "cpp" 8 | } 9 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode/.browse.c_cpp.db* 3 | .vscode/c_cpp_properties.json 4 | .vscode/launch.json 5 | .vscode/ipch 6 | pio.map 7 | output.map 8 | my.platformio.ini 9 | .vscode/extensions.json 10 | workspace.code-workspace 11 | -------------------------------------------------------------------------------- /src/credentials.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define USE_OTAA 4 | 5 | #include 6 | 7 | /* Do not edit this file at all! See credentials.cpp instead */ 8 | 9 | extern uint8_t devEui[8]; 10 | extern uint8_t appEui[8]; 11 | extern uint8_t appKey[16]; 12 | -------------------------------------------------------------------------------- /src/images.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define SATELLITE_IMAGE_WIDTH 16 4 | #define SATELLITE_IMAGE_HEIGHT 15 5 | extern const uint8_t SATELLITE_IMAGE[] PROGMEM; 6 | #define TTN_IMAGE_WIDTH 48 7 | #define TTN_IMAGE_HEIGHT 48 8 | extern const uint8_t TTN_IMAGE[] PROGMEM; 9 | extern const uint8_t helium_logo_bmp[] PROGMEM; 10 | -------------------------------------------------------------------------------- /workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "files.associations": { 9 | "*.tcc": "cpp", 10 | "fstream": "cpp", 11 | "iosfwd": "cpp", 12 | "type_traits": "cpp", 13 | "cstdio": "cpp", 14 | "*.inc": "cpp", 15 | "cypm.h": "c" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ], 7 | "unwantedRecommendations": [ 8 | "ms-vscode.cpptools-extension-pack" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PlatformIO Unit Testing and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PlatformIO Unit Testing: 11 | - https://docs.platformio.org/page/plus/unit-testing.html 12 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:cubecell_gps] 12 | platform = heltec-cubecell 13 | board = cubecell_gps 14 | framework = arduino 15 | build_flags = 16 | ; -Wall -Wno-comment 17 | -w 18 | -Wl,-Map,output.map 19 | 20 | monitor_speed = 115200 21 | board_build.arduino.lorawan.region = US915 22 | board_build.arduino.lorawan.class = CLASS_A 23 | board_build.arduino.lorawan.netmode = OTAA 24 | board_build.arduino.lorawan.adr = OFF 25 | board_build.arduino.lorawan.uplinkmode = UNCONFIRMED 26 | board_build.arduino.lorawan.net_reserve = OFF 27 | ;board_build.arduino.lorawan.rgb = ACTIVE 28 | board_build.arduino.lorawan.rgb = OFF 29 | board_build.arduino.lorawan.debug_level = FREQ 30 | ;board_build.arduino.lorawan.debug_level = FREQ_AND_DIO 31 | board_build.arduino.lorawan.at_support = OFF 32 | -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /src/credentials.cpp: -------------------------------------------------------------------------------- 1 | /* Credentials definition */ 2 | #include "credentials.h" 3 | 4 | #ifndef USE_OTAA 5 | #error "Only OTAA is supported for Helium!" 6 | #endif 7 | 8 | /* 9 | This is where you define the three key values that map your Device to the Helium Console. 10 | All three values must match between the code and the Console. 11 | 12 | There are two general ways to go about this: 13 | 1) Let the Console pick random values for one or all of them, and copy them here in the code. 14 | -or- 15 | 2) Define values here in the code, and then copy them to the Console to match. 16 | 17 | When the Mapper boots, it will show all three values in the Monitor console, like this: 18 | 19 | +DevEui=00072441043ABCDE(For OTAA Mode) 20 | +AppEui=6081F9BF908E2EA0(For OTAA Mode) 21 | +AppKey=CE3204B816687881BC91D8AD30AF3F55(For OTAA Mode) 22 | 23 | This format is suitable for copying the hex values from Terminal/Monitor and pasting directly into the Helium Console as-is. 24 | These are all MSB in the terminal, same as the Console. 25 | 26 | If you want to take the random Console values for a new device, and use them here, be sure to select (default): 27 | Device EUI: msb 28 | App EUI: msb 29 | App Key: msb 30 | in the Console, then click the arrows to expand the values with comma separators, then paste them below. 31 | */ 32 | 33 | // The DevEUI will be generated automatically based on the device macaddr, if these are all zero 34 | uint8_t devEui[8] = { 35 | 0, 0, 0, 0, 0, 0, 0, 0 36 | }; 37 | 38 | // This value is commonly shared between many devices of the same type or server. 39 | uint8_t appEui[8] = { 40 | 0x60, 0x81, 0xF9, 0xBF, 0x90, 0x8E, 0x2E, 0xA0 41 | }; 42 | 43 | // The key shown here is the Semtech default key. You should probably change it to a lesser-known (random) value 44 | uint8_t appKey[16] = { 45 | 0xCE, 0x32, 0x04, 0xB8, 0x16, 0x68, 0x79, 0x91, 0xBC, 0x91, 0xD8, 0xAD, 0x30, 0xAF, 0x3F, 0x55 46 | }; 47 | -------------------------------------------------------------------------------- /src/configuration.h: -------------------------------------------------------------------------------- 1 | // Version 2 | #define APP_NAME "Cube Mapper" 3 | #define APP_VERSION "MaxP v2.3.9" 4 | 5 | // ----------------------------------------------------------------------------- 6 | // CONFIGURATION 7 | // Stuff you might reasonably want to change is here: 8 | // ----------------------------------------------------------------------------- 9 | 10 | // The Mapper is always sending when it either moves some DISTANCE, or waits some TIME: 11 | #define MIN_DIST_M 70.0 // Minimum distance (meters) from the last sent location before we can send again. A hex is about 340m. 12 | #define MAX_TIME_S (5 * 60) // If no minimum movement, uplink will still be sent every N seconds 13 | 14 | // After a while, it still watches GPS, but turns the display OFF and sends slower: 15 | #define REST_WAIT_S (30 * 60) // If we still haven't moved in this many seconds, start sending even slower 16 | #define REST_TIME_S (20 * 60) // Slow resting uplink frequency in seconds 17 | #define REST_LOW_VOLTAGE 3.4 // Below this voltage, send more slowly regardless of movement 18 | 19 | // After a long while, shut down GPS & screen and go into deeper sleep, missing the initial movement. 20 | #define SLEEP_WAIT_S (4 * 60 * 60) // If we are not moving (or no GPS) for this long, go into deeper sleep 21 | #define SLEEP_TIME_S (20 * 60) // Wake up this often to power on the GPS and check for movement 22 | #define SLEEP_GPS_TIMEOUT_S 60 // How long to wait for a GPS fix each time we wake 23 | #define SLEEP_LOW_VOLTAGE 3.3 // Below this voltage, stay in deep sleep regardless of movement 24 | 25 | #define USB_POWER_VOLTAGE 4.10 // Above this voltage, assume we have unlimited power (4.1 is typical) 26 | #define GPS_LOST_WAIT_S ( 5 * 60) // How long after losing GPS do we call it lost? 27 | #define GPS_LOST_TIME_S (15 * 60) // How often to send Lost GPS packets? 28 | 29 | #define JOIN_TIMEOUT_S (1 * 60) // How long to spend trying to Join/re-Join 30 | #define JOIN_RETRY_TIME_S (1 * 60) // Join Timeout/Retry sleep interval 31 | 32 | // Deadzone defines a circular area where no map packets will originate. 33 | // Set Radius to zero to disable, or leave it enabled to select center position from menu. 34 | // (Thanks to @Woutch for the name) 35 | // Set it here or in your platformio.ini 36 | #ifndef DEADZONE_LAT 37 | #define DEADZONE_LAT 34.5678 38 | #endif 39 | #ifndef DEADZONE_LON 40 | #define DEADZONE_LON -123.4567 41 | #endif 42 | #define DEADZONE_RADIUS_M 500 // meters 43 | 44 | // ----------------------------------------------------------------------------- 45 | // Less-common Configuration items, but feel free to adjust.. 46 | // ----------------------------------------------------------------------------- 47 | #define VBAT_CORRECTION 1.004 // Edit this for calibrating your battery voltage 48 | #define MENU_TIMEOUT_MS 3000 // Menu timeout milliseconds 49 | #define LONG_PRESS_MS 500 // How long to hold the button to Select 50 | #define BATTERY_UPDATE_RATE_MS (15 * 1000) // How often to sample battery voltage 51 | #define SCREEN_UPDATE_RATE_MS 500 // Refresh OLED screen this often 52 | #define SCREEN_ON_TIME_MS (MENU_TIMEOUT_MS + 5000) // Keep screen on this long after button 53 | #define FPORT_MAPPER 2 // FPort for Mapper Uplink messages -- must match Helium Console Decoder script! 54 | #define FPORT_LOST_GPS 5 // FPort for no-GPS uplink messages -- do not feed to Mapper -------------------------------------------------------------------------------- /lib/Serial/HardwareSerial.h: -------------------------------------------------------------------------------- 1 | /* 2 | HardwareSerial.h - Hardware serial library for Wiring 3 | Copyright (c) 2006 Nicholas Zambetti. All right reserved. 4 | 5 | This library is free software; you can redistribute it and/or 6 | modify it under the terms of the GNU Lesser General Public 7 | License as published by the Free Software Foundation; either 8 | version 2.1 of the License, or (at your option) any later version. 9 | 10 | This library is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | Lesser General Public License for more details. 14 | 15 | You should have received a copy of the GNU Lesser General Public 16 | License along with this library; if not, write to the Free Software 17 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 | 19 | Modified 28 September 2010 by Mark Sproul 20 | Modified 14 August 2012 by Alarus 21 | Modified 3 December 2013 by Matthijs Kooijman 22 | Modified 18 December 2014 by Ivan Grokhotkov (esp8266 platform support) 23 | Modified 31 March 2015 by Markus Sattler (rewrite the code for UART0 + UART1 support in ESP8266) 24 | Modified 25 April 2015 by Thomas Flayols (add configuration different from 8N1 in ESP8266) 25 | Modified 13 October 2018 by Jeroen Döll (add baudrate detection) 26 | Baudrate detection example usage (detection on Serial1): 27 | void setup() { 28 | Serial.begin(115200); 29 | delay(100); 30 | Serial.println(); 31 | 32 | Serial1.begin(0, SERIAL_8N1, -1, -1, true, 11000UL); // Passing 0 for baudrate to detect it, the last parameter is a timeout in ms 33 | 34 | unsigned long detectedBaudRate = Serial1.baudRate(); 35 | if(detectedBaudRate) { 36 | Serial.printf("Detected baudrate is %lu\n", detectedBaudRate); 37 | } else { 38 | Serial.println("No baudrate detected, Serial1 will not work!"); 39 | } 40 | } 41 | 42 | Pay attention: the baudrate returned by baudRate() may be rounded, eg 115200 returns 115201 43 | */ 44 | 45 | #ifndef HardwareSerial_h 46 | #define HardwareSerial_h 47 | 48 | #include 49 | #include 50 | #include "Stream.h" 51 | #include "cytypes.h" 52 | #include 53 | #include 54 | 55 | 56 | #define UART_NUM_0 0 57 | #define UART_NUM_1 1 58 | 59 | #define UART_BUFF_SIZE 255 60 | #define UART_RX_SIZE (UART_BUFF_SIZE+1) 61 | 62 | typedef struct { 63 | uint8_t rx_buf[UART_RX_SIZE]; 64 | uint16_t rx_w; 65 | uint16_t rx_r; 66 | } uart_rxbuff_t; 67 | 68 | class HardwareSerial: public Stream 69 | { 70 | public: 71 | HardwareSerial(int8_t uart_nr); 72 | 73 | bool begin(uint32_t baud=115200, uint32_t config=SERIAL_8N1, int rxPin=-1, int txPin=-1, bool invert=false, unsigned long timeout_ms = 20000UL); 74 | void end(); 75 | void updateBaudRate(unsigned long baud); 76 | int available(void); 77 | void delayByte(void); 78 | int availableForWrite(void); 79 | int peek(void); 80 | int read(void); 81 | void flush(void); 82 | int read(uint8_t* buff, uint32_t timeout); 83 | // uint16_t readBytesUntil(char terminator, char *buffer, uint16_t length); 84 | size_t write(uint8_t); 85 | size_t write(const uint8_t *buffer, size_t size); 86 | 87 | inline size_t write(const char * s) 88 | { 89 | return write((uint8_t*) s, strlen(s)); 90 | } 91 | inline size_t write(unsigned long n) 92 | { 93 | return write((uint8_t) n); 94 | } 95 | inline size_t write(long n) 96 | { 97 | return write((uint8_t) n); 98 | } 99 | inline size_t write(unsigned int n) 100 | { 101 | return write((uint8_t) n); 102 | } 103 | inline size_t write(int n) 104 | { 105 | return write((uint8_t) n); 106 | } 107 | uint32_t baudRate(); 108 | operator bool() const; 109 | 110 | size_t setRxBufferSize(size_t); 111 | void setDebugOutput(bool); 112 | 113 | protected: 114 | int8_t _uart_num; 115 | uint32_t SerialBaud; 116 | int8_t _rx; 117 | int8_t _tx; 118 | //uart_t* _uart; 119 | }; 120 | 121 | #if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_SERIAL) 122 | extern HardwareSerial Serial; 123 | extern HardwareSerial Serial1; 124 | 125 | #endif 126 | 127 | #endif 128 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | BasedOnStyle: Google 3 | AccessModifierOffset: -1 4 | AlignAfterOpenBracket: Align 5 | AlignConsecutiveMacros: false 6 | AlignConsecutiveAssignments: false 7 | AlignConsecutiveBitFields: false 8 | AlignConsecutiveDeclarations: false 9 | AlignEscapedNewlines: Left 10 | AlignOperands: Align 11 | AlignTrailingComments: true 12 | AllowAllArgumentsOnNextLine: true 13 | AllowAllConstructorInitializersOnNextLine: true 14 | AllowAllParametersOfDeclarationOnNextLine: true 15 | AllowShortEnumsOnASingleLine: true 16 | AllowShortBlocksOnASingleLine: Never 17 | AllowShortCaseLabelsOnASingleLine: false 18 | AllowShortFunctionsOnASingleLine: Empty 19 | AllowShortLambdasOnASingleLine: All 20 | AllowShortIfStatementsOnASingleLine: Never 21 | AllowShortLoopsOnASingleLine: true 22 | AlwaysBreakAfterDefinitionReturnType: None 23 | AlwaysBreakAfterReturnType: None 24 | AlwaysBreakBeforeMultilineStrings: true 25 | AlwaysBreakTemplateDeclarations: Yes 26 | BinPackArguments: true 27 | BinPackParameters: true 28 | BraceWrapping: 29 | AfterCaseLabel: false 30 | AfterClass: false 31 | AfterControlStatement: Never 32 | AfterEnum: false 33 | AfterFunction: false 34 | AfterNamespace: false 35 | AfterObjCDeclaration: false 36 | AfterStruct: false 37 | AfterUnion: false 38 | AfterExternBlock: false 39 | BeforeCatch: false 40 | BeforeElse: false 41 | BeforeLambdaBody: false 42 | BeforeWhile: false 43 | IndentBraces: false 44 | SplitEmptyFunction: true 45 | SplitEmptyRecord: true 46 | SplitEmptyNamespace: true 47 | BreakBeforeBinaryOperators: None 48 | BreakBeforeBraces: Attach 49 | BreakBeforeInheritanceComma: false 50 | BreakInheritanceList: BeforeColon 51 | BreakBeforeTernaryOperators: true 52 | BreakConstructorInitializersBeforeComma: false 53 | BreakConstructorInitializers: BeforeColon 54 | BreakAfterJavaFieldAnnotations: false 55 | BreakStringLiterals: true 56 | ColumnLimit: 160 57 | CommentPragmas: '^ IWYU pragma:' 58 | CompactNamespaces: false 59 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 60 | ConstructorInitializerIndentWidth: 4 61 | ContinuationIndentWidth: 4 62 | Cpp11BracedListStyle: true 63 | DeriveLineEnding: true 64 | DerivePointerAlignment: true 65 | DisableFormat: false 66 | ExperimentalAutoDetectBinPacking: false 67 | FixNamespaceComments: true 68 | ForEachMacros: 69 | - foreach 70 | - Q_FOREACH 71 | - BOOST_FOREACH 72 | IncludeBlocks: Regroup 73 | IncludeCategories: 74 | - Regex: '^' 75 | Priority: 2 76 | SortPriority: 0 77 | - Regex: '^<.*\.h>' 78 | Priority: 1 79 | SortPriority: 0 80 | - Regex: '^<.*' 81 | Priority: 2 82 | SortPriority: 0 83 | - Regex: '.*' 84 | Priority: 3 85 | SortPriority: 0 86 | IncludeIsMainRegex: '([-_](test|unittest))?$' 87 | IncludeIsMainSourceRegex: '' 88 | IndentCaseLabels: true 89 | IndentCaseBlocks: false 90 | IndentGotoLabels: true 91 | IndentPPDirectives: None 92 | IndentExternBlock: AfterExternBlock 93 | IndentWidth: 2 94 | IndentWrappedFunctionNames: false 95 | InsertTrailingCommas: None 96 | JavaScriptQuotes: Leave 97 | JavaScriptWrapImports: true 98 | KeepEmptyLinesAtTheStartOfBlocks: false 99 | MacroBlockBegin: '' 100 | MacroBlockEnd: '' 101 | MaxEmptyLinesToKeep: 1 102 | NamespaceIndentation: None 103 | ObjCBinPackProtocolList: Never 104 | ObjCBlockIndentWidth: 2 105 | ObjCBreakBeforeNestedBlockParam: true 106 | ObjCSpaceAfterProperty: false 107 | ObjCSpaceBeforeProtocolList: true 108 | PenaltyBreakAssignment: 2 109 | PenaltyBreakBeforeFirstCallParameter: 1 110 | PenaltyBreakComment: 300 111 | PenaltyBreakFirstLessLess: 120 112 | PenaltyBreakString: 1000 113 | PenaltyBreakTemplateDeclaration: 10 114 | PenaltyExcessCharacter: 1000000 115 | PenaltyReturnTypeOnItsOwnLine: 200 116 | PointerAlignment: Left 117 | RawStringFormats: 118 | - Language: Cpp 119 | Delimiters: 120 | - cc 121 | - CC 122 | - cpp 123 | - Cpp 124 | - CPP 125 | - 'c++' 126 | - 'C++' 127 | CanonicalDelimiter: '' 128 | BasedOnStyle: google 129 | - Language: TextProto 130 | Delimiters: 131 | - pb 132 | - PB 133 | - proto 134 | - PROTO 135 | EnclosingFunctions: 136 | - EqualsProto 137 | - EquivToProto 138 | - PARSE_PARTIAL_TEXT_PROTO 139 | - PARSE_TEST_PROTO 140 | - PARSE_TEXT_PROTO 141 | - ParseTextOrDie 142 | - ParseTextProtoOrDie 143 | - ParseTestProto 144 | - ParsePartialTestProto 145 | CanonicalDelimiter: '' 146 | BasedOnStyle: google 147 | ReflowComments: true 148 | SortIncludes: true 149 | SortUsingDeclarations: true 150 | SpaceAfterCStyleCast: false 151 | SpaceAfterLogicalNot: false 152 | SpaceAfterTemplateKeyword: true 153 | SpaceBeforeAssignmentOperators: true 154 | SpaceBeforeCpp11BracedList: false 155 | SpaceBeforeCtorInitializerColon: true 156 | SpaceBeforeInheritanceColon: true 157 | SpaceBeforeParens: ControlStatements 158 | SpaceBeforeRangeBasedForLoopColon: true 159 | SpaceInEmptyBlock: false 160 | SpaceInEmptyParentheses: false 161 | SpacesBeforeTrailingComments: 2 162 | SpacesInAngles: false 163 | SpacesInConditionalStatement: false 164 | SpacesInContainerLiterals: true 165 | SpacesInCStyleCastParentheses: false 166 | SpacesInParentheses: false 167 | SpacesInSquareBrackets: false 168 | SpaceBeforeSquareBrackets: false 169 | BitFieldColonSpacing: Both 170 | Standard: Auto 171 | StatementMacros: 172 | - Q_UNUSED 173 | - QT_REQUIRE_VERSION 174 | TabWidth: 8 175 | UseCRLF: false 176 | UseTab: Never 177 | WhitespaceSensitiveMacros: 178 | - STRINGIZE 179 | - PP_STRINGIZE 180 | - BOOST_PP_STRINGIZE 181 | -------------------------------------------------------------------------------- /src/images.cpp: -------------------------------------------------------------------------------- 1 | #include "Arduino.h" 2 | #include "images.h" 3 | 4 | #define SATELLITE_IMAGE_WIDTH 16 5 | #define SATELLITE_IMAGE_HEIGHT 15 6 | const uint8_t SATELLITE_IMAGE[] PROGMEM = { 7 | 0x00, 0x08, 0x00, 0x1C, 0x00, 0x0E, 0x20, 0x07, 0x70, 0x02, 0xF8, 0x00, 8 | 0xF0, 0x01, 0xE0, 0x03, 0xC8, 0x01, 0x9C, 0x54, 0x0E, 0x52, 0x07, 0x48, 9 | 0x02, 0x26, 0x00, 0x10, 0x00, 0x0E 10 | }; 11 | 12 | #if 0 13 | #define TTN_IMAGE_WIDTH 48 14 | #define TTN_IMAGE_HEIGHT 48 15 | const uint8_t TTN_IMAGE[] PROGMEM = { 16 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 17 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 18 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 19 | 0x00, 0x00, 0xF8, 0x1F, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x00, 20 | 0x00, 0x80, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xE0, 0xFF, 0xFF, 0x07, 0x00, 21 | 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0x00, 0x00, 0xF8, 0xFF, 0xFF, 0x1F, 0x00, 22 | 0x00, 0xFC, 0xFF, 0xFF, 0x3F, 0x00, 0x00, 0xFE, 0xFF, 0x1F, 0x7F, 0x00, 23 | 0x00, 0xFE, 0xFF, 0x4F, 0x7C, 0x00, 0x00, 0xFF, 0xFF, 0xF7, 0xFD, 0x00, 24 | 0x00, 0xFF, 0x1F, 0xF0, 0xF9, 0x00, 0x80, 0xFF, 0x07, 0xF0, 0xFB, 0x01, 25 | 0x80, 0xFF, 0x03, 0xF0, 0xF9, 0x01, 0x80, 0xFF, 0xE1, 0xFF, 0xFC, 0x03, 26 | 0xC0, 0xFF, 0xF1, 0x1F, 0xFE, 0x03, 0xC0, 0xFF, 0x78, 0x0C, 0xFF, 0x03, 27 | 0xC0, 0xFF, 0x18, 0x18, 0xFF, 0x03, 0xC0, 0xFF, 0x18, 0x18, 0xFF, 0x03, 28 | 0xC0, 0xFF, 0x18, 0x18, 0xFF, 0x03, 0xC0, 0xFF, 0x38, 0x18, 0xFF, 0x03, 29 | 0xC0, 0xFF, 0x78, 0x8E, 0xFF, 0x03, 0xC0, 0x7F, 0xF8, 0x8F, 0xFF, 0x03, 30 | 0x80, 0xBF, 0xFF, 0x87, 0xFF, 0x01, 0x80, 0x9F, 0x0F, 0xE0, 0xFF, 0x01, 31 | 0x80, 0x9F, 0x0F, 0xF0, 0xFF, 0x01, 0x00, 0xBF, 0x4F, 0xF8, 0xFF, 0x00, 32 | 0x00, 0x3F, 0xE7, 0xFF, 0xFF, 0x00, 0x00, 0x7E, 0xF0, 0xFF, 0x7F, 0x00, 33 | 0x00, 0xFC, 0xFE, 0xFF, 0x3F, 0x00, 0x00, 0xFC, 0xFF, 0xFF, 0x3F, 0x00, 34 | 0x00, 0xF8, 0xFF, 0xFF, 0x1F, 0x00, 0x00, 0xF0, 0xFF, 0xFF, 0x0F, 0x00, 35 | 0x00, 0xC0, 0xFF, 0xFF, 0x03, 0x00, 0x00, 0x80, 0xFF, 0xFF, 0x01, 0x00, 36 | 0x00, 0x00, 0xFE, 0x7F, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x0B, 0x00, 0x00, 37 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 38 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 39 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 40 | }; 41 | #endif 42 | 43 | 44 | // Sweet Helium Logo 45 | const uint8_t helium_logo_bmp[] PROGMEM = { 46 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 47 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xff, 0x7f, 48 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 49 | 0x00, 0x00, 0x00, 0xfc, 0xff, 0xff, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 50 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 0xff, 0xff, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 51 | 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0xff, 0xff, 0x03, 0x3e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 52 | 0x00, 0x00, 0x00, 0x00, 0xe0, 0xff, 0xff, 0x01, 0x7c, 0x80, 0x03, 0x00, 0x00, 0xe0, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xff, 0xff, 0x71, 0x7c, 0x80, 53 | 0x03, 0x00, 0x00, 0xe0, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xff, 0x03, 0xf8, 0xf8, 0x80, 0x03, 0x00, 0x00, 0xe0, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 54 | 0xf8, 0xff, 0x00, 0xf8, 0xf8, 0x80, 0x03, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x7f, 0x00, 0xf8, 0xf8, 0x81, 0x03, 0x00, 0x00, 0xe0, 55 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x3f, 0x00, 0x78, 0xfc, 0x81, 0x03, 0x00, 0x00, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfc, 0x1f, 0xf8, 0x07, 56 | 0xfc, 0x83, 0xf3, 0x01, 0xfe, 0xe0, 0xe0, 0x38, 0x70, 0x98, 0x1f, 0x3f, 0xfc, 0x1f, 0x9c, 0x07, 0xfe, 0x83, 0xff, 0x03, 0xff, 0xe1, 0xe0, 0x38, 0x70, 0x98, 57 | 0xbf, 0x7f, 0xfc, 0x1f, 0x06, 0x86, 0xff, 0x83, 0x0f, 0x87, 0x83, 0xe1, 0xe0, 0x38, 0x70, 0x78, 0xf8, 0x70, 0xfc, 0x1f, 0x02, 0x84, 0xff, 0x83, 0x07, 0x87, 58 | 0x01, 0xe3, 0xe0, 0x38, 0x70, 0x38, 0x70, 0x60, 0xfc, 0x0f, 0x03, 0x84, 0xff, 0x83, 0x03, 0xc7, 0x01, 0xe3, 0xe0, 0x38, 0x70, 0x38, 0x70, 0x60, 0xfc, 0x0f, 59 | 0x03, 0x84, 0xff, 0x83, 0x03, 0xc7, 0x01, 0xe7, 0xe0, 0x38, 0x70, 0x38, 0x70, 0x60, 0xfc, 0x0f, 0x02, 0x84, 0xff, 0x83, 0x03, 0xc7, 0xff, 0xe7, 0xe0, 0x38, 60 | 0x70, 0x38, 0x70, 0x60, 0xfc, 0x1f, 0x06, 0x86, 0xff, 0x83, 0x03, 0xc7, 0xff, 0xe7, 0xe0, 0x38, 0x70, 0x38, 0x70, 0x60, 0xfc, 0x07, 0x0e, 0x83, 0xff, 0x83, 61 | 0x03, 0xc7, 0x01, 0xe0, 0xe0, 0x38, 0x70, 0x38, 0x70, 0x60, 0xfc, 0x03, 0xfe, 0x81, 0xff, 0x83, 0x03, 0xc7, 0x01, 0xe0, 0xe0, 0x38, 0x70, 0x38, 0x70, 0x60, 62 | 0xf8, 0xe3, 0x61, 0xc0, 0xff, 0x81, 0x03, 0x87, 0x01, 0xe3, 0xe0, 0x38, 0x70, 0x38, 0x70, 0x60, 0xf8, 0xf1, 0x01, 0xe0, 0xff, 0x81, 0x03, 0x87, 0x83, 0xe3, 63 | 0xe0, 0x70, 0x78, 0x38, 0x70, 0x60, 0xf8, 0xf1, 0x01, 0xf0, 0xff, 0x81, 0x03, 0x07, 0xff, 0xc1, 0xe3, 0xf0, 0x6f, 0x38, 0x70, 0x60, 0xf0, 0xf1, 0x01, 0xf8, 64 | 0xff, 0x80, 0x03, 0x07, 0x7e, 0x80, 0xe3, 0xe0, 0x67, 0x38, 0x70, 0x60, 0xf0, 0xe1, 0x08, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 65 | 0x00, 0x00, 0xe0, 0x03, 0xf8, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x07, 0xfc, 0xff, 0x3f, 0x00, 0x00, 0x00, 66 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0f, 0xff, 0xff, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0xff, 67 | 0xff, 0xff, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 68 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0xff, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0xff, 0x01, 0x00, 69 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 70 | 0x00, 0xc0, 0xff, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 71 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 72 | -------------------------------------------------------------------------------- /lib/Serial/HardwareSerial.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "Arduino.h" 7 | #include "HardwareSerial.h" 8 | #include "project.h" 9 | 10 | 11 | HardwareSerial Serial(UART_NUM_0); 12 | HardwareSerial Serial1(UART_NUM_1); 13 | 14 | uart_rxbuff_t _rxbuff[2]; 15 | 16 | HardwareSerial::HardwareSerial(int8_t uart_num) 17 | :_uart_num(uart_num) 18 | ,_rx(-1) 19 | ,_tx(-1) 20 | {} 21 | 22 | void writeRxToBuff0() 23 | { 24 | uint8_t uart_num = 0; 25 | while(UART_1_SpiUartGetRxBufferSize()) 26 | { 27 | if( ((_rxbuff[uart_num].rx_w + 1) % UART_RX_SIZE) != _rxbuff[uart_num].rx_r ) 28 | { 29 | _rxbuff[uart_num].rx_buf[_rxbuff[uart_num].rx_w ++] = UART_1_UartGetByte();; 30 | _rxbuff[uart_num].rx_w = _rxbuff[uart_num].rx_w % UART_RX_SIZE; 31 | } 32 | else 33 | { 34 | UART_1_UartGetByte(); 35 | } 36 | } 37 | UART_1_ClearRxInterruptSource(UART_1_INTR_RX_NOT_EMPTY); 38 | } 39 | 40 | void writeRxToBuff1() 41 | { 42 | uint8_t uart_num = 1; 43 | while(UART_2_SpiUartGetRxBufferSize()) 44 | { 45 | if( ((_rxbuff[uart_num].rx_w + 1) % UART_RX_SIZE) != _rxbuff[uart_num].rx_r ) 46 | { 47 | _rxbuff[uart_num].rx_buf[_rxbuff[uart_num].rx_w ++] = UART_2_UartGetByte();; 48 | _rxbuff[uart_num].rx_w = _rxbuff[uart_num].rx_w % UART_RX_SIZE; 49 | } 50 | else 51 | { 52 | UART_2_UartGetByte(); 53 | } 54 | } 55 | UART_2_ClearRxInterruptSource(UART_2_INTR_RX_NOT_EMPTY); 56 | } 57 | 58 | 59 | bool HardwareSerial::begin(uint32_t baud, uint32_t config, int rxPin, int txPin, bool invert, unsigned long timeout_ms) 60 | { 61 | if(_uart_num == 0 && rxPin < 0 && txPin < 0) { 62 | rxPin = UART_RX; 63 | txPin = UART_TX; 64 | } 65 | else if(rxPin == UART_RX && txPin == UART_TX) { 66 | _uart_num = 0; 67 | } 68 | #ifdef __ASR6502__ 69 | else if(_uart_num == 1 && rxPin < 0 && txPin < 0) { 70 | rxPin = UART_RX2; 71 | txPin = UART_TX2; 72 | } 73 | else if(rxPin == UART_RX2 && txPin == UART_TX2) { 74 | _uart_num = 1; 75 | } 76 | #endif 77 | else 78 | { 79 | return false; 80 | } 81 | 82 | _rx = rxPin; 83 | _tx = txPin; 84 | uint32_t stop_bits,parity_ctrl,data_bits,parity; 85 | 86 | stop_bits = config & 0x0F; 87 | parity = (config>>4) & 0x0F; 88 | data_bits = (config>>8) & 0x0F; 89 | 90 | parity_ctrl = ((UART_1_UART_PARITY_NONE != parity) ? \ 91 | (UART_1_GET_UART_RX_CTRL_PARITY(parity) | \ 92 | UART_1_UART_RX_CTRL_PARITY_ENABLED) : (0u)); 93 | 94 | uint32_t rx_ctrl0 = (UART_1_GET_UART_RX_CTRL_MODE(stop_bits) | \ 95 | UART_1_GET_UART_RX_CTRL_POLARITY(UART_1_UART_IRDA_POLARITY) | \ 96 | UART_1_GET_UART_RX_CTRL_MP_MODE(UART_1_UART_MP_MODE_ENABLE) | \ 97 | UART_1_GET_UART_RX_CTRL_DROP_ON_PARITY_ERR(UART_1_UART_DROP_ON_PARITY_ERR) | \ 98 | UART_1_GET_UART_RX_CTRL_DROP_ON_FRAME_ERR(UART_1_UART_DROP_ON_FRAME_ERR) | \ 99 | UART_1_GET_UART_RX_CTRL_BREAK_WIDTH(UART_1_UART_RX_BREAK_WIDTH) | \ 100 | parity_ctrl); 101 | 102 | uint32_t rx_ctrl1 =(UART_1_GET_RX_CTRL_DATA_WIDTH(data_bits) | \ 103 | UART_1_GET_RX_CTRL_MEDIAN (UART_1_UART_MEDIAN_FILTER_ENABLE) | \ 104 | UART_1_GET_UART_RX_CTRL_ENABLED(UART_1_UART_DIRECTION)); 105 | 106 | uint32_t tx_ctrl0 = (UART_1_GET_UART_TX_CTRL_MODE(stop_bits) | \ 107 | UART_1_GET_UART_TX_CTRL_RETRY_NACK(UART_1_UART_RETRY_ON_NACK) | \ 108 | parity_ctrl); 109 | 110 | uint32_t tx_ctrl1 = (UART_1_GET_TX_CTRL_DATA_WIDTH(data_bits) | \ 111 | UART_1_GET_UART_TX_CTRL_ENABLED(UART_1_UART_DIRECTION)); 112 | 113 | SerialBaud=baud; 114 | 115 | if( _uart_num == UART_NUM_0) 116 | { 117 | //if(digitalRead(UART_RX)==UART_RX_LEVEL)//uart start when uart chip powered 118 | //{ 119 | uint32_t div = (float)CYDEV_BCLK__HFCLK__HZ/SerialBaud/UART_1_UART_OVS_FACTOR + 0.5 - 1; 120 | UART_1_Start(); 121 | UART_1_SCBCLK_DIV_REG = div<<8; 122 | UART_1_UART_RX_CTRL_REG = rx_ctrl0; 123 | UART_1_RX_CTRL_REG =rx_ctrl1; 124 | UART_1_UART_TX_CTRL_REG =tx_ctrl0; 125 | UART_1_TX_CTRL_REG = tx_ctrl1; 126 | //} 127 | } 128 | else 129 | { 130 | uint32_t div = (float)CYDEV_BCLK__HFCLK__HZ/SerialBaud/UART_2_UART_OVS_FACTOR + 0.5 - 1; 131 | UART_2_Start(); 132 | UART_2_SCBCLK_DIV_REG = div<<8; 133 | UART_2_UART_RX_CTRL_REG = rx_ctrl0; 134 | UART_2_RX_CTRL_REG =rx_ctrl1; 135 | UART_2_UART_TX_CTRL_REG =tx_ctrl0; 136 | UART_2_TX_CTRL_REG = tx_ctrl1; 137 | } 138 | _rxbuff[_uart_num].rx_r=0; 139 | _rxbuff[_uart_num].rx_w=0; 140 | // _rxbuff[_uart_num].rx_buf=(uint8_t*) malloc(UART_RX_SIZE); 141 | 142 | if( _uart_num == UART_NUM_0) 143 | { 144 | UART_1_SCB_IRQ_StartEx(writeRxToBuff0); 145 | 146 | } 147 | else 148 | { 149 | UART_2_SCB_IRQ_StartEx(writeRxToBuff1); 150 | } 151 | 152 | 153 | return true; 154 | /* 155 | printf("config %d\r\n",config); 156 | printf("stop_bits %d\r\n",stop_bits); 157 | printf("parity %d\r\n",parity); 158 | printf("data_bits %d\r\n",data_bits); 159 | */ 160 | } 161 | 162 | void HardwareSerial::updateBaudRate(unsigned long baud) 163 | { 164 | SerialBaud = baud; 165 | if( _uart_num == UART_NUM_0) 166 | { 167 | uint32_t div = (float)CYDEV_BCLK__HFCLK__HZ/SerialBaud/UART_1_UART_OVS_FACTOR + 0.5 - 1; 168 | UART_1_SCBCLK_DIV_REG = div<<8; 169 | } 170 | else 171 | { 172 | uint32_t div = (float)CYDEV_BCLK__HFCLK__HZ/SerialBaud/UART_2_UART_OVS_FACTOR + 0.5 - 1; 173 | UART_2_SCBCLK_DIV_REG = div<<8; 174 | } 175 | } 176 | 177 | void HardwareSerial::end() 178 | { 179 | if( _uart_num == UART_NUM_0) 180 | { 181 | UART_1_Stop(); 182 | } 183 | else 184 | { 185 | UART_2_Stop(); 186 | } 187 | 188 | // free(_rxbuff[_uart_num].rx_buf); // Max_Plastix from @Kicko fix 189 | } 190 | 191 | size_t HardwareSerial::setRxBufferSize(size_t new_size) { 192 | // return uartResizeRxBuffer(_uart, new_size); 193 | return 0; 194 | } 195 | 196 | void HardwareSerial::setDebugOutput(bool en) 197 | { 198 | /* if(_uart == 0) { 199 | return; 200 | } 201 | if(en) { 202 | uartSetDebug(_uart); 203 | } else { 204 | if(uartGetDebug() == _uart_nr) { 205 | uartSetDebug(0); 206 | } 207 | }*/ 208 | } 209 | 210 | int HardwareSerial::available(void) 211 | { 212 | /* 213 | 214 | uint8_t buffsize; 215 | //for(uint32_t i=0;i<(23040000/SerialBaud);i++) 216 | //{ 217 | 218 | if( _uart_num == UART_NUM_0) 219 | { 220 | buffsize=UART_1_SpiUartGetRxBufferSize(); 221 | } 222 | else 223 | { 224 | buffsize=UART_2_SpiUartGetRxBufferSize(); 225 | } 226 | if(buffsize){ 227 | return buffsize; 228 | } 229 | //} 230 | return 0;*/ 231 | 232 | if(_rxbuff[_uart_num].rx_r == _rxbuff[_uart_num].rx_w) 233 | return 0; 234 | if(_rxbuff[_uart_num].rx_r < _rxbuff[_uart_num].rx_w) 235 | return _rxbuff[_uart_num].rx_w - _rxbuff[_uart_num].rx_r; 236 | else 237 | return UART_RX_SIZE - _rxbuff[_uart_num].rx_r + _rxbuff[_uart_num].rx_w; 238 | } 239 | 240 | void HardwareSerial::delayByte(void) 241 | { 242 | delayMicroseconds(11000000/SerialBaud); 243 | } 244 | 245 | int HardwareSerial::availableForWrite(void) 246 | { 247 | if( _uart_num == UART_NUM_0) 248 | { 249 | return (int) (UART_1_TX_BUFFER_SIZE - UART_1_SpiUartGetTxBufferSize()); 250 | } 251 | else 252 | { 253 | return (int) (UART_2_TX_BUFFER_SIZE - UART_2_SpiUartGetTxBufferSize()); 254 | } 255 | } 256 | 257 | int HardwareSerial::peek(void) 258 | { 259 | if(_rxbuff[_uart_num].rx_r == _rxbuff[_uart_num].rx_w) 260 | return -1; 261 | return _rxbuff[_uart_num].rx_buf[_rxbuff[_uart_num].rx_r]; 262 | } 263 | 264 | int HardwareSerial::read(void) 265 | { 266 | /* 267 | if(available()) { 268 | if( _uart_num == UART_NUM_0) 269 | { 270 | return UART_1_UartGetByte(); 271 | } 272 | else 273 | { 274 | return UART_2_UartGetByte(); 275 | } 276 | } 277 | return (uint32)(-1);*/ 278 | if(_rxbuff[_uart_num].rx_r == _rxbuff[_uart_num].rx_w) 279 | return -1; 280 | 281 | uint8_t data = _rxbuff[_uart_num].rx_buf[_rxbuff[_uart_num].rx_r++]; 282 | _rxbuff[_uart_num].rx_r = _rxbuff[_uart_num].rx_r % UART_RX_SIZE; 283 | return data; 284 | } 285 | 286 | int HardwareSerial::read(uint8_t* buff, uint32_t timeout) 287 | { 288 | uint32_t timestart = millis(); 289 | while(available()==0) 290 | { 291 | if( (millis()-timestart) > timeout ) 292 | { 293 | return 0; 294 | } 295 | } 296 | int serialBuffer_index=0; 297 | while(available()) 298 | { 299 | buff[serialBuffer_index++]=read(); 300 | 301 | int i = 0; 302 | while(i<1000) 303 | { 304 | if(available()) 305 | break; 306 | delayMicroseconds(1); 307 | i++; 308 | } 309 | } 310 | return serialBuffer_index; 311 | //Serial.write(serialBuffer,serialBuffer_index); 312 | } 313 | 314 | 315 | void HardwareSerial::flush() 316 | { 317 | _rxbuff[_uart_num].rx_r = 0; 318 | _rxbuff[_uart_num].rx_w = 0; 319 | if( _uart_num == UART_NUM_0) 320 | { 321 | UART_1_SpiUartClearRxBuffer(); 322 | while (UART_1_SpiUartGetTxBufferSize()); 323 | UART_1_SpiUartClearTxBuffer(); 324 | } 325 | else 326 | { 327 | UART_2_SpiUartClearRxBuffer(); 328 | while (UART_2_SpiUartGetTxBufferSize()); 329 | UART_2_SpiUartClearTxBuffer(); 330 | } 331 | } 332 | 333 | size_t HardwareSerial::write(uint8_t c) 334 | { 335 | if( _uart_num == UART_NUM_0) 336 | { 337 | UART_1_UartPutChar(c); 338 | } 339 | else 340 | { 341 | UART_2_UartPutChar(c); 342 | } 343 | return 1; 344 | } 345 | 346 | size_t HardwareSerial::write(const uint8_t *buffer, size_t size) 347 | { 348 | uint32 bufIndex; 349 | bufIndex = 0u; 350 | 351 | while(bufIndex < size) 352 | { 353 | if( _uart_num == UART_NUM_0) 354 | { 355 | UART_1_UartPutChar( buffer[bufIndex] ); 356 | } 357 | else 358 | { 359 | UART_2_UartPutChar( buffer[bufIndex] ); 360 | } 361 | bufIndex++; 362 | } 363 | return size; 364 | } 365 | uint32_t HardwareSerial::baudRate() 366 | { 367 | //return uartGetBaudRate(_uart); 368 | return SerialBaud; 369 | } 370 | HardwareSerial::operator bool() const 371 | { 372 | return true; 373 | //return 0; 374 | } 375 | 376 | 377 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | Helium Mapper build for the **Heltec CubeCell GPS-6502 HTCC-AB02S** 3 | 4 | A Mapper device helps determine the Helium network coverage area. Learn more about Mapping in general here: https://docs.helium.com/use-the-network/coverage-mapping/ 5 | 6 | This software is based on the CubeCell GPS example from Heltec's examples, 7 | and on Jas Williams version from https://github.com/jas-williams/CubeCell-Helium-Mapper.git with GPS Distance and 8 | improvements from https://github.com/hkicko/CubeCell-GPS-Helium-Mapper 9 | This build copies some look and behavior from my TTGO T-Beam build at https://github.com/Max-Plastix/tbeam-helium-mapper. 10 | 11 | ## Note on hardware 12 | If you have not yet bought any Helium Mapper hardware, consider the LilyGo TTGO T-Beam instead of the Heltec CubeCell. 13 | The cost is similar, but the Heltec uses **closed-source binaries** in their Platform libraries while TTGO is open-source with ESP32. The TTGO also has a superior GPS antenna, more buttons, WiFi, Bluetooth, and a power management IC. The Heltec CubeCell is physically smaller and lower power in sleep. Study the tradeoffs. 14 | 15 | ### CubeCell Version 16 | Heltec has released multiple versions of the CubeCell GPS units. This software works only with the `CubeCell GPS 6502 v1.1`. You can see the different versions [here](https://heltec-automation-docs.readthedocs.io/en/latest/cubecell/dev-board/htcc-ab02s/hardware_update_log.html#v1-1). The v1.1 has been for sale since early 2021, and has an AIR530Z GPS module. If you have one of the older 2020 boards, then this build won't work on it. 17 | 18 | ## Usage 19 | The CubeCell only has one User Button, so a short press steps to the next menu entry, and a long press selects that entry. 20 | 21 | ------ Everything below is copied from Kicko's https://github.com/hkicko/CubeCell-GPS-Helium-Mapper 22 | # Uploading the code 23 | 24 | **Note: If you prefer to use Arduino IDE, just take the \src\main.cpp file and rename it to "something".ino (for example CubeCell_GPS_Helium_Mapper.ino)** 25 | 26 | Install Serial Driver. Find directions [here.](https://heltec-automation-docs.readthedocs.io/en/latest/general/establish_serial_connection.html) 27 | 28 | Install [Visual Studio Code](https://code.visualstudio.com/Download). If you are using Windows, make sure the pick the System installer, not the User installer. 29 | 30 | (Optional) When the Get Started wizard prompts you to install language extensions - install the C/C++ extension. 31 | 32 | Install Git from https://git-scm.com/downloads or https://github.com/git-guides/install-git 33 | 34 | Reboot your computer for the path changes to take effect. 35 | 36 | Install the GitHub Pull Requests and Issues extension from the link [here](https://code.visualstudio.com/docs/editor/github). 37 | 38 | Install [PlatformIO IDE](https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide) 39 | 40 | Once you are in Visual Studio Code, go to the Explorer and click Clone Repository. Paste the URL you got from GitHub, by clicking on the Code button. When prompted for location, if you want to use the default location (and you are using Windows) do the following - select your Documents folder, if there is no PlatformIO sub-folder in it - create one and enter it, then if there is no Projects sub-folder inside - create it and select it as the location for the cloned repository. So the final location would be %userprofile%\Documents\PlatformIO\Projects 41 | 42 | Open the cloned folder 43 | 44 | Open the main.cpp from src sub-folder and wait. Initially the #include directives at the top will have squiggly lines as unknown, but relatively soon (within 5 min) PlatformIO will detect and install the required platform and libraries. If you don't want to wait, open PlatformIO and go to Platforms and install "ASR Microelectronics ASR650x". You can do that as a step right after installing PlatformIO. 45 | 46 | Comment out/uncomment the appropriate line for your board version (for GPS Air530 or Air530Z) in main.cpp. 47 | 48 | Comment out/uncomment the #define lines for VIBR_SENSOR, VIBR_WAKE_FROM_SLEEP, MENU_SLEEP_DISABLE_VIBR_WAKEUP, MAX_GPS_WAIT, MAX_STOPPED_CYCLES and edit the values for the timers if desired. 49 | 50 | Enter DevEUI(msb), AppEUI(msb), and AppKey(msb) from Helium Console, at the respective places in main.cpp. The values must be in MSB format. From console press the expand button to get the ID's as shown below. 51 | 52 | ![Console Image](https://gblobscdn.gitbook.com/assets%2F-M21bzsbFl2WA7VymAxU%2F-M6fLGmWEQ0QxjrJuvoC%2F-M6fLi5NzuMeWSzzihV-%2Fcubecell-console-details.png?alt=media&token=95f5c9b2-734a-4f84-bb88-523215873116) 53 | 54 | ``` 55 | uint8_t devEui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; 56 | uint8_t appEui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; 57 | uint8_t appKey[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; 58 | ``` 59 | 60 | Modify platformio.ini if you need to change LoRaWAN settings like region. 61 | 62 | Click the PlatformIO: Build button. Address any compile errors and repeat until you get a clean build. 63 | 64 | Connect the CubeCell to the computer with USB cable. 65 | 66 | Click the PlatformIO: Upload button. 67 | 68 | # Debug using Serial connection via USB 69 | 70 | (Optional) Uncomment the line enabling the DEBUG code and build again. 71 | ``` 72 | //#define DEBUG // Enable/Disable debug output over the serial console 73 | ``` 74 | Click the PlatformIO: Serial Monitor button 75 | 76 | # Setting up Console 77 | 78 | In [Helium Console](https://console.helium.com/) create a new function call it Heltec decoder => Type Decoder => Custom Script 79 | 80 | Copy and paste the decoder into the custom script pane 81 | 82 | ``` 83 | // Decoder for MaxPlastix mappers 84 | // 85 | // 11 Byte payload: 86 | // 3 Lat, 3 Long, 2 Altitude (m), 1 Speed (km/hr), 1 Battery, 1 Sats. 87 | // Accuracy is a dummy value required by some Integrations. 88 | // Battery is 1/100 of a volt, offset by 2v for a range of 2.00 to 4.56 volts. 89 | // 90 | function Decoder(bytes, port) { 91 | var decoded = {}; 92 | 93 | // All formats carry a lat & lon reading: 94 | var latitude = ((bytes[0] << 16) >>> 0) + ((bytes[1] << 8) >>> 0) + bytes[2]; 95 | latitude = (latitude / 16777215.0 * 180) - 90; 96 | 97 | var longitude = ((bytes[3] << 16) >>> 0) + ((bytes[4] << 8) >>> 0) + bytes[5]; 98 | longitude = (longitude / 16777215.0 * 360) - 180; 99 | 100 | switch (port) { 101 | case 2: // Mapper! (Cargo and Heatmap too) 102 | decoded.latitude = latitude; 103 | decoded.longitude = longitude; 104 | 105 | var altValue = ((bytes[6] << 8) >>> 0) + bytes[7]; 106 | var sign = bytes[6] & (1 << 7); 107 | if (sign) 108 | decoded.altitude = 0xFFFF0000 | altValue; 109 | else 110 | decoded.altitude = altValue; 111 | 112 | decoded.speed = parseFloat((((bytes[8])) / 1.609).toFixed(2)); 113 | decoded.battery = parseFloat((bytes[9] / 100 + 2).toFixed(2)); 114 | decoded.sats = bytes[10]; 115 | decoded.accuracy = 2.5; // Bogus Accuracy required by Cargo/Mapper integration 116 | break; 117 | case 5: // System status 118 | decoded.last_latitude = latitude; 119 | decoded.last_longitude = longitude; 120 | decoded.battery = parseFloat((bytes[6] / 100 + 2).toFixed(2)); 121 | decoded.value = bytes[8]; 122 | decoded.status = bytes[7]; 123 | switch (bytes[7]) { 124 | case 1: 125 | decoded.status = "BOOT"; 126 | break; 127 | case 2: 128 | decoded.status = "USB ON"; 129 | break 130 | case 3: 131 | decoded.status = "USB OFF"; 132 | break; 133 | } 134 | break; 135 | case 6: // Lost GPS 136 | decoded.last_latitude = latitude; 137 | decoded.last_longitude = longitude; 138 | decoded.battery = parseFloat((bytes[6] / 100 + 2).toFixed(2)); 139 | decoded.sats = bytes[7]; 140 | decoded.minutes = ((bytes[8] << 8) >>> 0) + bytes[9]; 141 | break; 142 | } 143 | 144 | return decoded; 145 | } 146 | ``` 147 | 148 | Create two integrations one for CARGO (optional) and one for MAPPERS. 149 | For CARGO use the available prebuilt integration. 150 | For MAPPERS use a custom HTTP integration with POST Endpoint URL https://mappers.helium.com/api/v1/ingest/uplink 151 | 152 | Go to Flows and from the Nodes menu add your device, decoder function and integrations. 153 | Connect the device to the decoder. 154 | Connect the decoder to the integrations. 155 | 156 | Useful links: 157 | 158 | [Mappers](http://mappers.helium.com) and [Cargo](https://cargo.helium.com) 159 | 160 | [Integration information with Mappers](https://docs.helium.com/use-the-network/coverage-mapping/mappers-api/) 161 | 162 | [Integration information for Cargo](https://docs.helium.com/use-the-network/console/integrations/cargo/) 163 | 164 | # Google WebApp integration 165 | 166 | Example integration for Google WebApp to populate a spreadsheet with decoded output 167 | 168 | ```function doPost(e) { 169 | var GS = SpreadsheetApp.openById('') 170 | // Create a sheet for today if it doesn't exist and add column headers 171 | var SheetDate = new Date().toLocaleDateString(); 172 | if (!GS.getSheetByName(SheetDate)) 173 | GS.insertSheet(SheetDate).getRange('A1:N1').setValues([[ 174 | 'Time', 'DateTime', 'Device EUI', 'Device Name', 'Battery', 175 | 'Latitude', 'Longitude', 'Sats', 'Speed', 176 | 'Hotspot', 'RSSI', 'SNR', 'Hotspot Dist', 'Hotspot Count' 177 | ]]); 178 | 179 | // Get all contents 180 | var json = JSON.parse(e.postData.contents); 181 | 182 | if (json.port == 2) 183 | var ThisSheet = GS.getSheetByName(SheetDate); 184 | else if (json.port == 5) 185 | var ThisSheet = GS.getSheetByName('Status'); 186 | else if (json.port == 6) 187 | var ThisSheet = GS.getSheetByName('Lost GPS'); 188 | else 189 | var ThisSheet = GS.getSheetByName('Unknown'); 190 | 191 | // Row place holder 192 | var ThisRecord = []; 193 | var i = 0; 194 | 195 | ThisRecord[i++] = new Date().toLocaleTimeString(); // Timestamp 196 | ThisRecord[i++] = new Date().toLocaleString(); // DateTime 197 | ThisRecord[i++] = json.dev_eui; // EUI 198 | ThisRecord[i++] = json.name; // Device Name 199 | ThisRecord[i++] = json.decoded.payload.battery; // Battery 200 | 201 | if (json.port == 2) { 202 | ThisRecord[i++] = json.decoded.payload.latitude; // Latitude 203 | ThisRecord[i++] = json.decoded.payload.longitude; // Longitude 204 | ThisRecord[i++] = json.decoded.payload.sats; // Sats 205 | ThisRecord[i++] = json.decoded.payload.speed; // Speed 206 | //ThisRecord[i++] = json.decoded.payload.accuracy; // Accuracy stuck at 2.5 207 | } else if (json.port == 5) { 208 | ThisRecord[i++] = json.decoded.payload.last_latitude; // Latitude 209 | ThisRecord[i++] = json.decoded.payload.last_longitude; // Longitude 210 | ThisRecord[i++] = json.decoded.payload.status; 211 | ThisRecord[i++] = json.decoded.payload.value; 212 | } else if (json.port == 6) { 213 | ThisRecord[i++] = json.decoded.payload.last_latitude; // Latitude 214 | ThisRecord[i++] = json.decoded.payload.last_longitude; // Longitude 215 | ThisRecord[i++] = json.decoded.payload.sats; 216 | ThisRecord[i++] = json.decoded.payload.minutes; 217 | } else { 218 | ThisRecord[i++] = json.port; 219 | ThisRecord[i++] = json.payload; 220 | ThisRecord[i++] = json.payload_size; 221 | } 222 | 223 | ThisRecord[i++] = json.hotspots[0].name; //Hotspot Name 224 | // ThisRecord[i++] = json.hotspots[0].lat; //Hotspot Latitude 225 | // ThisRecord[i++] = json.hotspots[0].long; //Hotspot Longitude 226 | ThisRecord[i++] = json.hotspots[0].rssi; //Hotspot RSSI 227 | ThisRecord[i++] = json.hotspots[0].snr; //Hotspot SNR 228 | 229 | // Distance to Hotspot 230 | var lat1 = Number(json.decoded.payload.latitude); 231 | var lon1 = Number(json.decoded.payload.longitude); 232 | var lat2 = Number(json.hotspots[0].lat); 233 | var lon2 = Number(json.hotspots[0].long); 234 | var R = 6378.137; // Radius of earth in KM 235 | var dLat = lat2 * Math.PI / 180 - lat1 * Math.PI / 180; 236 | var dLon = lon2 * Math.PI / 180 - lon1 * Math.PI / 180; 237 | var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + 238 | Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * 239 | Math.sin(dLon / 2) * Math.sin(dLon / 2); 240 | var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 241 | var d = R * c; 242 | ThisRecord[i++] = (d * 1000); 243 | 244 | ThisRecord[i++] = json.hotspots.length; // How many hotspots heard this? 245 | 246 | 247 | // Save in spreadsheet 248 | ThisSheet.getRange(ThisSheet.getLastRow() + 1, 1, 1, ThisRecord.length).setValues([ThisRecord]); 249 | } 250 | ``` 251 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Max-Plastix Mapper build for 3 | * Heltec CubeCell GPS-6502 HTCC-AB02S 4 | * 5 | * Based on the CubeCell GPS example from 6 | * libraries\LoRa\examples\LoRaWAN\LoRaWAN_Sensors\LoRaWan_OnBoardGPS_Air530\LoRaWan_OnBoardGPS_Air530.ino 7 | * and on Jas Williams version from 8 | * https://github.com/jas-williams/CubeCell-Helium-Mapper.git with GPS Distance 9 | * and improvements from https://github.com/hkicko/CubeCell-GPS-Helium-Mapper 10 | * 11 | * Apologies for inconsistent style and naming; there were many hands in this code and Heltec has quite the Platform library. 12 | * 13 | */ 14 | 15 | #include "Arduino.h" 16 | /* CubeCell bundlesfonts into their standard Platform library. 17 | * Unfortunately, it declares them right in the.h Header file itself, and in their library binary, 18 | * so we have to tolerate getting TWO copies of each font included in flash. */ 19 | #include "HT_Display.h" 20 | #include "HT_SSD1306Wire.h" // HelTec's old version of SSD1306Wire.h, pulls in HT_DisplayFonts.h 21 | 22 | #include "GPS_Air530Z.h" // Bastard plagerized Heltec packed-in library 23 | #include "LoRaWan_APP.h" // Global values for IDs and LoRa state 24 | #include "configuration.h" // User configuration 25 | #include "credentials.h" // Helium LoRaWAN credentials 26 | #include "cyPm.h" // For deep sleep 27 | #include "hw.h" // for CyDelay() 28 | #include "timeServer.h" // Timer handlers 29 | 30 | //#include "TinyGPS++.h" // More recent/superior library 31 | 32 | #include "images.h" // For Satellite icon 33 | 34 | extern SSD1306Wire display; // Defined in LoRaWan_APP.cpp (!) 35 | extern uint8_t isDispayOn; // [sic] Defined in LoRaWan_APP.cpp 36 | #define isDisplayOn isDispayOn // Seriously. It's wrong all over LoRaWan_APP. 37 | 38 | extern uint32_t UpLinkCounter; // FCnt, Frame Count, Uplink Sequence Number 39 | extern bool wakeByUart; // Declared in no-source Heltec binary that was 40 | // \cores\asr650x\projects\CubeCellLib.a(AT_Command.o):D:\lib\650x/AT_Command.c:54 41 | extern HardwareSerial GPSSerial; // Defined in GPSTrans.cpp as UART_NUM_1 42 | Air530ZClass AirGPS; // Just for Init. Has some Air530Z specials. 43 | // TinyGPSPlus GPS; // Avoid the Heltec library as much as we can 44 | #define GPS AirGPS 45 | 46 | #if defined(REGION_EU868) 47 | /*LoraWan channelsmask, default channels 0-7*/ 48 | uint16_t userChannelsMask[6] = {0x00FF, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}; 49 | #else 50 | uint16_t userChannelsMask[6] = {0xFF00, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000}; 51 | #endif 52 | 53 | /* To represent battery voltage in one payload byte we scale it to hundredths of a volt, 54 | * with an offset of 2.0 volts, giving a useable range of 2.0 to 4.56v, perfect for any 55 | * Lithium battery technology. */ 56 | #define ONE_BYTE_BATTERY_V(mV) ((uint8_t)(((mV + 5) / 10) - 200) & 0xFF) 57 | 58 | /* Unset APB stuff, but CubeCellLib.a requires that we declare them */ 59 | uint8_t nwkSKey[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 60 | uint8_t appSKey[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; 61 | uint32_t devAddr = 0; 62 | 63 | /* LoRaWAN Configuration. Edit in platformio.ini */ 64 | LoRaMacRegion_t loraWanRegion = ACTIVE_REGION; 65 | DeviceClass_t loraWanClass = LORAWAN_CLASS; 66 | 67 | /* Application data transmission duty cycle. value in [ms]. */ 68 | uint32_t appTxDutyCycle = 1000 - 100; // Must be a bit less than GPS update rate! 69 | 70 | /* OTAA or ABP. (Helium only supports OTAA) */ 71 | bool overTheAirActivation = LORAWAN_NETMODE; 72 | 73 | /* ADR enable */ 74 | bool loraWanAdr = LORAWAN_ADR; 75 | 76 | /* set LORAWAN_Net_Reserve ON, the node could save the network info to flash, 77 | * when node reset not need to join again */ 78 | bool keepNet = LORAWAN_NET_RESERVE; 79 | 80 | /* Indicates if the node is sending confirmed or unconfirmed messages */ 81 | bool isTxConfirmed = LORAWAN_UPLINKMODE; 82 | 83 | /* Application port. NOTE: Name is fixed, as LoRa library will extern ours (!) */ 84 | uint8_t appPort = 0; 85 | 86 | /*! 87 | Number of trials to transmit the frame, if the LoRaMAC layer did not 88 | receive an acknowledgment. The MAC performs a datarate adaptation, 89 | according to the LoRaWAN Specification V1.0.2, chapter 18.4, according 90 | to the following table: 91 | Transmission nb | Data Rate 92 | ----------------|----------- 93 | 1 (first) | DR 94 | 2 | DR 95 | 3 | max(DR-1,0) 96 | 4 | max(DR-1,0) 97 | 5 | max(DR-2,0) 98 | 6 | max(DR-2,0) 99 | 7 | max(DR-3,0) 100 | 8 | max(DR-3,0) 101 | Note, that if NbTrials is set to 1 or 2, the MAC will not decrease 102 | the datarate, in case the LoRaMAC layer did not receive an acknowledgment 103 | */ 104 | uint8_t confirmedNbTrials = 4; 105 | 106 | static TimerEvent_t BatteryUpdateTimer; 107 | static TimerEvent_t ScreenUpdateTimer; 108 | static TimerEvent_t KeyDownTimer; 109 | static TimerEvent_t MenuIdleTimer; 110 | static TimerEvent_t DeepSleepTimer; 111 | static TimerEvent_t ScreenOnTimer; 112 | static TimerEvent_t JoinFailTimer; 113 | 114 | float min_dist_moved = MIN_DIST_M; // Meters of movement that count as moved 115 | uint32_t max_time_ms = MAX_TIME_S * 1000; // Time interval of Map uplinks 116 | uint32_t rest_time_ms = REST_TIME_S * 1000; // slower time interval (screen off) 117 | uint32_t sleep_time_ms = SLEEP_TIME_S * 1000; // slowest time interval (deep sleep) 118 | uint32_t tx_time_ms = max_time_ms; // The currently active send interval 119 | 120 | // Deadzone (no uplink) location and radius 121 | double deadzone_lat = DEADZONE_LAT; 122 | double deadzone_lon = DEADZONE_LON; 123 | double deadzone_radius_m = DEADZONE_RADIUS_M; 124 | boolean in_deadzone = false; 125 | 126 | boolean in_menu = false; // Menu currently displayed? 127 | const char *menu_prev; // Previous menu name 128 | const char *menu_cur; // Highlighted menu name 129 | const char *menu_next; // Next menu name 130 | boolean is_highlighted = false; // highlight the current menu entry 131 | int menu_entry = 0; // selected item 132 | boolean go_menu_select = false; // Do the menu thing(in main context) 133 | 134 | boolean justSendNow = false; // Send an uplink right now 135 | 136 | boolean key_down = false; // User Key pressed right now? 137 | uint32_t keyDownTime; // how long was it pressed for 138 | void userKeyIRQ(void); // (soft) interrupt handler for key press 139 | volatile boolean keyIRQ = false; // IRQ flag for key event 140 | boolean long_press = false; // did this count as a Long press? 141 | 142 | uint32_t last_fix_ms = 0; // When did we last get a good GPS fix? 143 | double last_send_lat = 0.0; // Last sent Latitude 144 | double last_send_lon = 0.0; // Last sent Longitude 145 | uint32_t last_send_ms; // time of last uplink 146 | boolean is_joined = false; // True after Join complete 147 | 148 | boolean need_light_sleep = false; // Should we be in low-power state? 149 | uint32_t need_deep_sleep_s = 0; // Should we be in lowest-power state? 150 | boolean in_light_sleep = false; // Are we presently in low-power 151 | boolean in_deep_sleep = false; // Are we in deepest sleep? 152 | boolean hold_screen_on = false; // Are we holding the screen on from key press? 153 | boolean stay_on = false; // Has the user asked the screen to STAY on? 154 | 155 | boolean is_gps_lost = false; // No GPS Fix? 156 | uint32_t last_lost_gps_ms = 0; // When did we last cry about no GPS 157 | uint32_t last_moved_ms = 0; // When did we last notice significant movement 158 | uint16_t battery_mv; // Last measured battery voltage in millivolts 159 | 160 | char buffer[40]; // Scratch string buffer for display strings 161 | 162 | void onDeepSleepTimer(void); 163 | void onJoinFailTimer(void); 164 | void deepest_sleep(uint32_t sleepfor_s); 165 | 166 | // SK6812 (WS2812). DIN is IO13/GP13/Pin45 167 | void testRGB(void) { 168 | for (uint32_t i = 0; i <= 30; i++) { 169 | turnOnRGB(i << 16, 10); 170 | } 171 | for (uint32_t i = 0; i <= 30; i++) { 172 | turnOnRGB(i << 8, 10); 173 | } 174 | for (uint32_t i = 0; i <= 30; i++) { 175 | turnOnRGB(i, 10); 176 | } 177 | turnOnRGB(0, 0); 178 | } 179 | 180 | // RGB LED power on (Vext to SK5812 pin 4) 181 | void VextON(void) { 182 | pinMode(Vext, OUTPUT); 183 | digitalWrite(Vext, LOW); 184 | } 185 | 186 | // RGB LED power off 187 | void VextOFF(void) { 188 | pinMode(Vext, OUTPUT); 189 | digitalWrite(Vext, HIGH); 190 | } 191 | 192 | void printGPSInfo(void) { 193 | // Serial.print("Date/Time: "); 194 | if (GPS.date.isValid()) { 195 | Serial.printf("%d-%02d-%02d", GPS.date.year(), GPS.date.month(), GPS.date.day()); 196 | } else { 197 | Serial.print("INVALID"); 198 | } 199 | 200 | if (GPS.time.isValid()) { 201 | Serial.printf(" %02d:%02d:%02d.%02d", GPS.time.hour(), GPS.time.minute(), GPS.time.second(), GPS.time.centisecond()); 202 | } else { 203 | Serial.print(" INVALID"); 204 | } 205 | Serial.println(); 206 | 207 | #if 1 208 | Serial.print(" LAT: "); 209 | Serial.print(GPS.location.lat(), 6); 210 | Serial.print(", LON: "); 211 | Serial.print(GPS.location.lng(), 6); 212 | Serial.print(", ALT: "); 213 | Serial.print(GPS.altitude.meters()); 214 | 215 | Serial.print(", HDOP: "); 216 | Serial.print(GPS.hdop.hdop()); 217 | Serial.print(", AGE: "); 218 | Serial.print(GPS.location.age()); 219 | Serial.print(", COURSE: "); 220 | Serial.print(GPS.course.deg()); 221 | Serial.print(", SPEED: "); 222 | Serial.print(GPS.speed.kmph()); 223 | #endif 224 | Serial.print(", Sats: "); 225 | Serial.print(GPS.satellites.value()); 226 | 227 | Serial.println(); 228 | } 229 | 230 | void displayLogoAndMsg(String msg, uint32_t wait_ms) { 231 | display.clear(); 232 | display.drawXbm(0, 0, 128, 42, helium_logo_bmp); 233 | // displayBatteryLevel(); 234 | display.setTextAlignment(TEXT_ALIGN_CENTER); 235 | display.setFont(ArialMT_Plain_16); 236 | display.drawString(64, 54 - 16 / 2, msg); 237 | Serial.println(msg); 238 | display.display(); 239 | 240 | if (wait_ms) { 241 | delay(wait_ms); 242 | } 243 | } 244 | 245 | boolean screenOffMode = false; 246 | 247 | void switchScreenOffMode() { 248 | screenOffMode = true; 249 | VextOFF(); 250 | display.stop(); 251 | isDisplayOn = 0; 252 | } 253 | 254 | void switchScreenOnMode() { 255 | screenOffMode = false; 256 | VextON(); 257 | isDisplayOn = 1; 258 | display.init(); 259 | display.clear(); 260 | display.display(); 261 | } 262 | 263 | // Fetch Data Rate? (DR_0 to DR_5) 264 | int8_t loraDataRate(void) { 265 | MibRequestConfirm_t mibReq; 266 | LoRaMacStatus_t status; 267 | int8_t ret = -1; 268 | 269 | mibReq.Type = MIB_CHANNELS_DATARATE; 270 | status = LoRaMacMibGetRequestConfirm(&mibReq); 271 | if (status == LORAMAC_STATUS_OK) { 272 | ret = mibReq.Param.ChannelsDatarate; 273 | } 274 | 275 | return ret; 276 | } 277 | 278 | #define SCREEN_HEADER_HEIGHT 24 279 | uint8_t _screen_line = SCREEN_HEADER_HEIGHT - 1; 280 | SSD1306Wire *disp; 281 | 282 | void draw_screen(void); 283 | 284 | void onScreenUpdateTimer(void) { 285 | draw_screen(); 286 | TimerReset(&ScreenUpdateTimer); 287 | TimerStart(&ScreenUpdateTimer); 288 | } 289 | 290 | void screen_print(const char *text, uint8_t x, uint8_t y, uint8_t alignment) { 291 | if (isDisplayOn) { 292 | disp->setTextAlignment((DISPLAY_TEXT_ALIGNMENT)alignment); 293 | disp->drawString(x, y, text); 294 | } 295 | } 296 | 297 | void screen_print(const char *text, uint8_t x, uint8_t y) { 298 | screen_print(text, x, y, TEXT_ALIGN_LEFT); 299 | } 300 | 301 | void screen_print(const char *text) { 302 | Serial.printf(">>> %s", text); 303 | 304 | if (isDisplayOn) { 305 | disp->print(text); 306 | if (_screen_line + 8 > disp->getHeight()) { 307 | // scroll 308 | } 309 | _screen_line += 8; 310 | } 311 | } 312 | 313 | void screen_setup() { 314 | // Display instance 315 | disp = &display; 316 | disp->setFont(ArialMT_Plain_10); 317 | 318 | // Scroll buffer 319 | disp->setLogBuffer(4, 40); 320 | } 321 | 322 | uint32_t gps_start_time; 323 | 324 | void configure_gps(void) { 325 | // Adjust the Green 1pps LED to have shorter blinks. 326 | const uint8_t cmdbuf[] = {0xBA, 0xCE, 0x10, 0x00, 0x06, 0x03, 0x40, 0x42, 0x0F, 0x00, 0x10, 0x27, 0x00, 327 | 0x00, 0x03, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x63, 0x69, 0x15, 0x0B}; 328 | /* CFG-TX Time Pulse: 10mS width (vs 100), only if fixed, UTC Time, Automatic source */ 329 | GPSSerial.write(cmdbuf, sizeof(cmdbuf)); 330 | delay(50); 331 | 332 | AirGPS.setmode(MODE_GPS_BEIDOU_GLONASS); 333 | delay(50); 334 | 335 | AirGPS.setNMEA(NMEA_RMC | NMEA_GGA); // Eliminate unused message traffic like SV 336 | delay(50); 337 | 338 | #if 0 339 | // GPSSerial.write("$PCAS02,500*1A\r\n"); /* 500mS updates */ 340 | GPSSerial.write("$PCAS02,1000*2E\r\n"); /* 1S updates */ 341 | GPSSerial.flush(); 342 | delay(50); 343 | #endif 344 | } 345 | 346 | void start_gps(void) { 347 | Serial.println("Starting GPS:"); 348 | AirGPS.begin(115200); // Faster messages; less CPU, more idle. 349 | configure_gps(); 350 | gps_start_time = millis(); 351 | } 352 | 353 | void fast_start_gps() { 354 | Serial.println("(Wake GPS)"); 355 | pinMode(GPIO14, OUTPUT); 356 | digitalWrite(GPIO14, LOW); 357 | GPSSerial.begin(115200); // Faster messages; less CPU, more idle. 358 | gps_start_time = millis(); 359 | while (GPS.getNMEA() == "0") { 360 | if (millis() - gps_start_time > SLEEP_GPS_TIMEOUT_S * 1000) 361 | return; 362 | } 363 | configure_gps(); 364 | gps_start_time = millis(); 365 | } 366 | 367 | void update_gps() { 368 | static boolean firstfix = true; 369 | uint32_t now_fix_count; 370 | static uint32_t last_fix_count = 0; 371 | 372 | while (GPSSerial.available()) { 373 | int c = GPSSerial.read(); 374 | if (c > 0) { 375 | char c8 = c; 376 | // Serial.print(c8); 377 | GPS.encode(c8); 378 | } 379 | } 380 | 381 | now_fix_count = GPS.sentencesWithFix(); 382 | if (now_fix_count != last_fix_count && GPS.location.isValid() && GPS.time.isValid() && GPS.date.isValid()) { 383 | last_fix_ms = millis(); 384 | if (firstfix) { 385 | firstfix = false; 386 | snprintf(buffer, sizeof(buffer), "GPS fix: %d sec\n", (last_fix_ms - gps_start_time) / 1000); 387 | screen_print(buffer); 388 | printGPSInfo(); 389 | } 390 | } 391 | } 392 | 393 | void stopGPS() { 394 | AirGPS.end(); 395 | } 396 | 397 | /* In Heltec's brilliance, they put the User Key button input on top of the Battery ADC with a complicated switch, 398 | * instead of using one of the many other GPIOs that would not conflict at all. Because of this, we should 399 | * ignore fake keypress inputs while sampling the battery ADC. The getBatteryVoltage() call will also turn the 400 | * ADC Control switch over to Battery with pinMode(VBAT_ADC_CTL,OUTPUT) and restore it after. 401 | * Note that battery measurement takes significant time, so should be done infrequently and outside of any critical 402 | * LoRa timing. 403 | */ 404 | void update_battery_mv(void) { 405 | detachInterrupt(USER_KEY); // ignore phantom button pushes 406 | battery_mv = (getBatteryVoltage() * (1024.0 * VBAT_CORRECTION)) / 1024; 407 | attachInterrupt(USER_KEY, userKeyIRQ, BOTH); 408 | Serial.printf("Bat: %d mV\n", battery_mv); 409 | } 410 | 411 | void onBatteryUpdateTimer(void) { 412 | update_battery_mv(); 413 | TimerReset(&BatteryUpdateTimer); 414 | TimerStart(&BatteryUpdateTimer); 415 | } 416 | 417 | /* Populate appData/appDataSize with Mapper frame */ 418 | boolean prepare_map_uplink(uint8_t port) { 419 | uint32_t lat, lon; 420 | int alt, speed, sats; 421 | 422 | unsigned char *puc; 423 | 424 | appDataSize = 0; 425 | 426 | if (!GPS.location.isValid()) 427 | return false; 428 | 429 | last_send_lat = GPS.location.lat(); 430 | last_send_lon = GPS.location.lng(); 431 | 432 | lat = ((last_send_lat + 90) / 180.0) * 16777215; 433 | lon = ((last_send_lon + 180) / 360.0) * 16777215; 434 | 435 | alt = (uint16_t)GPS.altitude.meters(); 436 | // course = GPS.course.deg(); 437 | speed = (uint16_t)GPS.speed.kmph(); 438 | sats = GPS.satellites.value(); 439 | // hdop = GPS.hdop.hdop(); 440 | 441 | puc = (unsigned char *)(&lat); 442 | appData[appDataSize++] = puc[2]; 443 | appData[appDataSize++] = puc[1]; 444 | appData[appDataSize++] = puc[0]; 445 | 446 | puc = (unsigned char *)(&lon); 447 | appData[appDataSize++] = puc[2]; 448 | appData[appDataSize++] = puc[1]; 449 | appData[appDataSize++] = puc[0]; 450 | 451 | puc = (unsigned char *)(&alt); 452 | appData[appDataSize++] = puc[1]; 453 | appData[appDataSize++] = puc[0]; 454 | 455 | puc = (unsigned char *)(&speed); 456 | appData[appDataSize++] = puc[0]; 457 | 458 | appData[appDataSize++] = ONE_BYTE_BATTERY_V(battery_mv); 459 | 460 | appData[appDataSize++] = (uint8_t)(sats & 0xFF); 461 | return true; 462 | } 463 | 464 | void gps_passthrough(void) { 465 | Serial.println("GPS Passthrough forever..."); 466 | int c; 467 | while (1) { 468 | c = GPSSerial.read(); 469 | if (c != -1) 470 | Serial.write(c); 471 | 472 | c = Serial.read(); 473 | if (c != -1) { 474 | GPSSerial.write(c); } 475 | } 476 | } 477 | 478 | struct menu_item { 479 | const char *name; 480 | void (*func)(void); 481 | }; 482 | 483 | void menu_send_now(void) { 484 | justSendNow = true; 485 | } 486 | 487 | void menu_power_off(void) { 488 | screen_print("\nPOWER OFF...\n"); 489 | delay(1000); // Give some time to read the screen 490 | VextOFF(); 491 | pinMode(Vext, ANALOG); 492 | pinMode(ADC, ANALOG); 493 | display.sleep(); 494 | display.displayOff(); 495 | display.stop(); 496 | detachInterrupt(RADIO_DIO_1); 497 | isDisplayOn = 0; 498 | AirGPS.end(); 499 | 500 | Radio.Sleep(); 501 | TimerStop(&KeyDownTimer); 502 | TimerStop(&BatteryUpdateTimer); 503 | TimerStop(&ScreenUpdateTimer); 504 | TimerStop(&MenuIdleTimer); 505 | TimerStop(&DeepSleepTimer); 506 | TimerStop(&ScreenOnTimer); 507 | 508 | // Must Reset to exit 509 | wakeByUart = false; 510 | while (1) lowPowerHandler(); 511 | } 512 | 513 | void menu_distance_plus(void) { 514 | min_dist_moved += 10; 515 | } 516 | void menu_distance_minus(void) { 517 | min_dist_moved -= 10; 518 | if (min_dist_moved < 10) 519 | min_dist_moved = 10; 520 | } 521 | void menu_time_plus(void) { 522 | max_time_ms += 60 * 1000; 523 | } 524 | void menu_time_minus(void) { 525 | max_time_ms -= 60 * 1000; 526 | if (max_time_ms < 60 * 1000) 527 | max_time_ms = 60 * 1000; 528 | } 529 | void menu_gps_passthrough(void) { 530 | gps_passthrough(); 531 | // Does not return! 532 | } 533 | void menu_deadzone_here(void) { 534 | if (GPS.location.isValid()) { 535 | deadzone_lat = GPS.location.lat(); 536 | deadzone_lon = GPS.location.lng(); 537 | } 538 | } 539 | void menu_stay_on(void) { 540 | stay_on = !stay_on; 541 | } 542 | 543 | void menu_experiment(void) { 544 | screen_print("\nExperiment..\n"); 545 | 546 | delay(2000); 547 | keyIRQ = false; 548 | deepest_sleep(5); 549 | 550 | screen_print("Done\n"); 551 | 552 | // uint32 reason = CySysGetResetReason(); 553 | } 554 | 555 | struct menu_item menu[] = {{"Send Now", menu_send_now}, {"Power Off", menu_power_off}, {"Distance +10", menu_distance_plus}, 556 | {"Distance -10", menu_distance_minus}, {"Time +60", menu_time_plus}, {"Time -60", menu_time_minus}, 557 | {"Deadzone Here", menu_deadzone_here}, {"USB GPS", menu_gps_passthrough}, {"Stay ON", menu_stay_on}, 558 | //{"Experiment", menu_experiment} 559 | }; 560 | #define MENU_ENTRIES (sizeof(menu) / sizeof(menu[0])) 561 | 562 | void onMenuIdleTimer(void) { 563 | in_menu = false; 564 | is_highlighted = false; 565 | } 566 | 567 | void onScreenOnTimer(void) { 568 | hold_screen_on = false; 569 | } 570 | 571 | void menu_press(void) { 572 | if (in_menu) 573 | menu_entry = (menu_entry + 1) % MENU_ENTRIES; 574 | else 575 | in_menu = true; 576 | 577 | menu_prev = menu[(menu_entry - 1) % MENU_ENTRIES].name; 578 | menu_cur = menu[menu_entry].name; 579 | menu_next = menu[(menu_entry + 1) % MENU_ENTRIES].name; 580 | 581 | draw_screen(); 582 | TimerReset(&MenuIdleTimer); 583 | TimerStart(&MenuIdleTimer); 584 | } 585 | 586 | void menu_selected(void) { 587 | if (in_menu) { 588 | is_highlighted = true; 589 | draw_screen(); 590 | menu[menu_entry].func(); // Might not return. 591 | } else { 592 | in_menu = true; 593 | draw_screen(); 594 | } 595 | } 596 | 597 | void menu_deselected(void) { 598 | is_highlighted = false; 599 | draw_screen(); 600 | } 601 | 602 | void onKeyDownTimer(void) { 603 | // Long Press! 604 | long_press = true; 605 | go_menu_select = true; 606 | } 607 | 608 | // Interrupt handler for button press 609 | void userKeyIRQ(void) { 610 | keyIRQ = true; 611 | } 612 | 613 | void userKeyIRQ_process(void) { 614 | if (!keyIRQ) 615 | return; 616 | keyIRQ = false; 617 | 618 | if (!key_down && digitalRead(USER_KEY) == LOW) { 619 | // Key Pressed 620 | key_down = true; 621 | long_press = false; 622 | keyDownTime = millis(); 623 | TimerReset(&KeyDownTimer); 624 | TimerStart(&KeyDownTimer); 625 | } 626 | 627 | if (key_down && digitalRead(USER_KEY) == HIGH) { 628 | // Key Released 629 | key_down = false; 630 | if (long_press) { 631 | menu_deselected(); 632 | } else { 633 | TimerStop(&KeyDownTimer); // Cancel timer 634 | menu_press(); 635 | } 636 | #if 1 637 | uint32_t press_ms = millis() - keyDownTime; 638 | Serial.printf("[Key Pressed %d ms.]\n", press_ms); 639 | #endif 640 | } 641 | TimerReset(&MenuIdleTimer); 642 | 643 | hold_screen_on = true; 644 | TimerStop(&ScreenOnTimer); 645 | TimerReset(&ScreenOnTimer); 646 | TimerStart(&ScreenOnTimer); 647 | } 648 | 649 | void gps_time(char *buffer, uint8_t size) { 650 | snprintf(buffer, size, "%02d:%02d:%02d", GPS.time.hour(), GPS.time.minute(), GPS.time.second()); 651 | } 652 | 653 | void screen_header(void) { 654 | uint32_t sats; 655 | uint32_t now = millis(); 656 | char bat_level = '?'; 657 | 658 | sats = GPS.satellites.value(); 659 | 660 | // Cycle display every 3 seconds 661 | if (millis() % 6000 < 3000) { 662 | // 2 bytes of Device EUI with Voltage and Current 663 | snprintf(buffer, sizeof(buffer), "#%04X", (devEui[6] << 8) | devEui[7]); 664 | disp->setTextAlignment(TEXT_ALIGN_LEFT); 665 | disp->drawString(0, 2, buffer); 666 | 667 | snprintf(buffer, sizeof(buffer), "%d.%02dV", battery_mv / 1000, (battery_mv % 1000) / 10); 668 | } else { 669 | if (!GPS.time.isValid() || sats < 3) 670 | snprintf(buffer, sizeof(buffer), "*** NO GPS ***"); 671 | else 672 | gps_time(buffer, sizeof(buffer)); 673 | } 674 | 675 | disp->setTextAlignment(TEXT_ALIGN_CENTER); 676 | disp->drawString(disp->getWidth() / 2, 2, buffer); 677 | 678 | // Satellite count 679 | disp->setTextAlignment(TEXT_ALIGN_RIGHT); 680 | disp->drawString(disp->getWidth() - SATELLITE_IMAGE_WIDTH - 4, 2, itoa(sats, buffer, 10)); 681 | disp->drawXbm(disp->getWidth() - SATELLITE_IMAGE_WIDTH, 0, SATELLITE_IMAGE_WIDTH, SATELLITE_IMAGE_HEIGHT, SATELLITE_IMAGE); 682 | 683 | if (battery_mv > USB_POWER_VOLTAGE * 1000) 684 | bat_level = 'U'; 685 | else if (battery_mv > REST_LOW_VOLTAGE * 1000) 686 | bat_level = 'H'; 687 | else if (battery_mv > SLEEP_LOW_VOLTAGE * 1000) 688 | bat_level = 'L'; 689 | else 690 | bat_level = 'Z'; 691 | 692 | // Second status row: 693 | snprintf(buffer, sizeof(buffer), "%ds / %ds %dm %c%c%c%c\n", 694 | (now - last_send_ms) / 1000, // Time since last send 695 | tx_time_ms / 1000, // Interval Time 696 | (int)min_dist_moved, // Interval Distance 697 | bat_level, // U for Unlimited Power (USB), Hi, Low, Zero 698 | stay_on ? 'S' : '-', // S for Screen Stay ON 699 | in_deadzone ? 'D' : '-', // D for Deadzone 700 | !is_joined ? 'X' : '-' // X for Not Joined Yet 701 | ); 702 | disp->setTextAlignment(TEXT_ALIGN_LEFT); 703 | disp->drawString(0, 12, buffer); 704 | 705 | // disp->setTextAlignment(TEXT_ALIGN_RIGHT); 706 | // disp->drawString(disp->getWidth(), 12, cached_sf_name); 707 | 708 | disp->drawHorizontalLine(0, SCREEN_HEADER_HEIGHT, disp->getWidth()); 709 | } 710 | 711 | #define MARGIN 15 712 | 713 | void draw_screen(void) { 714 | disp->setFont(ArialMT_Plain_10); 715 | disp->clear(); 716 | screen_header(); 717 | 718 | if (in_menu) { 719 | disp->setTextAlignment(TEXT_ALIGN_CENTER); 720 | disp->drawString(disp->getWidth() / 2, SCREEN_HEADER_HEIGHT + 5, menu_prev); 721 | disp->drawString(disp->getWidth() / 2, SCREEN_HEADER_HEIGHT + 28, menu_next); 722 | // if (is_highlighted) 723 | // disp->clear(); 724 | disp->drawHorizontalLine(MARGIN, SCREEN_HEADER_HEIGHT + 16, disp->getWidth() - MARGIN * 2); 725 | snprintf(buffer, sizeof(buffer), is_highlighted ? ">>> %s <<<" : "%s", menu_cur); 726 | disp->drawString(disp->getWidth() / 2, SCREEN_HEADER_HEIGHT + 16, buffer); 727 | disp->drawHorizontalLine(MARGIN, SCREEN_HEADER_HEIGHT + 28, disp->getWidth() - MARGIN * 2); 728 | disp->drawVerticalLine(MARGIN, SCREEN_HEADER_HEIGHT + 16, 28 - 16); 729 | disp->drawVerticalLine(disp->getWidth() - MARGIN, SCREEN_HEADER_HEIGHT + 16, 28 - 16); 730 | } else { 731 | disp->drawLogBuffer(0, SCREEN_HEADER_HEIGHT); 732 | } 733 | disp->display(); 734 | } 735 | 736 | void setup() { 737 | boardInitMcu(); 738 | 739 | Serial.begin(115200); 740 | 741 | if (!(devEui[0] || devEui[1] || devEui[2] || devEui[3] || devEui[4] || devEui[5] || devEui[6] || devEui[7])) 742 | LoRaWAN.generateDeveuiByChipID(); // Overwrite devEui with chip-unique value 743 | 744 | #if (STAGING_CONSOLE) 745 | devEui[0] = 0xBB; 746 | #endif 747 | 748 | #if (AT_SUPPORT) 749 | enableAt(); 750 | #endif 751 | 752 | isDisplayOn = 1; 753 | display.init(); // displayMcuInit() will init the display, but if we want to 754 | // show our logo before that, we need to init ourselves. 755 | displayLogoAndMsg(APP_VERSION, 100); 756 | 757 | start_gps(); // GPS takes the longest to settle. 758 | 759 | LoRaWAN.displayMcuInit(); // This inits and turns on the display 760 | deviceState = DEVICE_STATE_INIT; 761 | 762 | /* This will switch deviceState to DEVICE_STATE_SLEEP and schedule a SEND 763 | timer which will switch to DEVICE_STATE_SEND if saved network info exists 764 | and no new JOIN is necessary */ 765 | /* Unfortunately, it uses the inscrutible checkNetInfo() binary-only function, which appears to have bugs. */ 766 | // LoRaWAN.ifskipjoin(); 767 | 768 | // Setup user button - this must be after LoRaWAN.ifskipjoin(), because the 769 | // button is used there to cancel stored settings load and initiate a new join 770 | pinMode(USER_KEY, INPUT); 771 | attachInterrupt(USER_KEY, userKeyIRQ, BOTH); 772 | 773 | screen_setup(); 774 | screen_print(APP_VERSION "\n"); 775 | 776 | TimerInit(&KeyDownTimer, onKeyDownTimer); 777 | TimerSetValue(&KeyDownTimer, LONG_PRESS_MS); 778 | 779 | TimerInit(&MenuIdleTimer, onMenuIdleTimer); 780 | TimerSetValue(&MenuIdleTimer, MENU_TIMEOUT_MS); 781 | 782 | TimerInit(&DeepSleepTimer, onDeepSleepTimer); 783 | //TimerSetValue(&DeepSleepTimer, SLEEP_TIME_S * 1000); 784 | 785 | TimerInit(&ScreenOnTimer, onScreenOnTimer); 786 | TimerSetValue(&ScreenOnTimer, SCREEN_ON_TIME_MS); 787 | 788 | TimerInit(&JoinFailTimer, onJoinFailTimer); 789 | // TimerSetValue(&JoinFailTimer, 2 * JOIN_TIMEOUT_S * 1000); 790 | 791 | TimerInit(&BatteryUpdateTimer, onBatteryUpdateTimer); 792 | TimerSetValue(&BatteryUpdateTimer, BATTERY_UPDATE_RATE_MS); 793 | TimerStart(&BatteryUpdateTimer); 794 | update_battery_mv(); 795 | 796 | TimerInit(&ScreenUpdateTimer, onScreenUpdateTimer); 797 | TimerSetValue(&ScreenUpdateTimer, SCREEN_UPDATE_RATE_MS); 798 | TimerStart(&ScreenUpdateTimer); // Let's Go! 799 | } 800 | 801 | boolean send_lost_uplink() { 802 | uint32 now = millis(); 803 | Serial.printf("Lost GPS %ds ago\n", (now - last_fix_ms) / 1000); 804 | unsigned char *puc; 805 | 806 | // Use last-known location; might be zero 807 | double lat = ((last_send_lat + 90) / 180.0) * 16777215; 808 | double lon = ((last_send_lon + 180) / 360.0) * 16777215; 809 | uint8_t sats = GPS.satellites.value(); 810 | uint16_t lost_minutes = MIN(0xFFFF, (now - last_fix_ms) / 1000 / 60); 811 | 812 | appPort = FPORT_LOST_GPS; 813 | appDataSize = 0; 814 | puc = (unsigned char *)(&lat); 815 | appData[appDataSize++] = puc[2]; 816 | appData[appDataSize++] = puc[1]; 817 | appData[appDataSize++] = puc[0]; 818 | 819 | puc = (unsigned char *)(&lon); 820 | appData[appDataSize++] = puc[2]; 821 | appData[appDataSize++] = puc[1]; 822 | appData[appDataSize++] = puc[0]; 823 | 824 | appData[appDataSize++] = ONE_BYTE_BATTERY_V(battery_mv); 825 | 826 | appData[appDataSize++] = (uint8_t)(sats & 0xFF); 827 | 828 | puc = (unsigned char *)(&lost_minutes); 829 | appData[appDataSize++] = puc[1]; 830 | appData[appDataSize++] = puc[0]; 831 | 832 | snprintf(buffer, sizeof(buffer), "%d NO-GPS %dmin\n", UpLinkCounter, lost_minutes); 833 | screen_print(buffer); 834 | 835 | last_lost_gps_ms = now; 836 | LoRaWAN.send(); 837 | 838 | return true; 839 | } 840 | 841 | boolean send_uplink(void) { 842 | uint32_t now = millis(); 843 | 844 | if (is_gps_lost && (now - last_fix_ms < GPS_LOST_WAIT_S * 1000)) { 845 | // Recovered 846 | screen_print("Found GPS\n"); 847 | is_gps_lost = false; 848 | } 849 | if (!is_gps_lost && (now - last_fix_ms > GPS_LOST_WAIT_S * 1000)) { 850 | // Haven't seen GPS in a while. Send a non-Mapper packet 851 | screen_print("Lost GPS!\n"); 852 | is_gps_lost = true; 853 | return send_lost_uplink(); 854 | } 855 | if (is_gps_lost && (now - last_lost_gps_ms) > GPS_LOST_TIME_S * 1000) { 856 | // Still Lost. Continue crying about it every GPS_LOST_TIME_S 857 | return send_lost_uplink(); 858 | } 859 | if (is_gps_lost && (now - last_fix_ms) > SLEEP_WAIT_S * 1000) { 860 | // Been lost a really long time. Can't tell if we're moving. Sleep 861 | need_light_sleep = true; 862 | need_deep_sleep_s = GPS_LOST_TIME_S; 863 | return false; 864 | } 865 | 866 | // Shouldn't happen, but if it does.. can't compute distance 867 | if (!GPS.location.isValid()) 868 | return false; 869 | 870 | double lat = GPS.location.lat(); 871 | double lon = GPS.location.lng(); 872 | 873 | double dist_moved = GPS.distanceBetween(last_send_lat, last_send_lon, lat, lon); 874 | double deadzone_dist = GPS.distanceBetween(deadzone_lat, deadzone_lon, lat, lon); 875 | in_deadzone = (deadzone_dist <= deadzone_radius_m); 876 | 877 | #if 1 878 | Serial.printf("[Time %d / %ds, Moved %d / %dm in %ds %c %c %c]\n", 879 | (now - last_send_ms) / 1000, // Time 880 | tx_time_ms / 1000, // interval 881 | (int32_t)dist_moved, // moved 882 | (int32_t)min_dist_moved, 883 | (now - last_moved_ms) / 1000, // last movement ago 884 | in_deadzone ? 'D' : 'd', need_light_sleep ? 'S' : 's', need_deep_sleep_s != 0 ? 'Z' : 'z'); 885 | #endif 886 | 887 | /* Set tx interval (and screen state!) based on time since last movement: */ 888 | if ((now - last_moved_ms < REST_WAIT_S * 1000) && (battery_mv > REST_LOW_VOLTAGE * 1000)) { 889 | // If we recently moved and battery is good.. keep the update rate high and screen on 890 | tx_time_ms = max_time_ms; 891 | need_light_sleep = false; 892 | need_deep_sleep_s = 0; 893 | } else if (battery_mv > USB_POWER_VOLTAGE * 1000) { 894 | // Don't slow down on USB power, or topped-off battery, ever 895 | tx_time_ms = max_time_ms; 896 | // However, OLED screens can burn-in, so do turn it off while stationary 897 | need_light_sleep = (now - last_moved_ms > REST_WAIT_S * 1000); 898 | need_deep_sleep_s = 0; 899 | } else if (now - last_moved_ms > SLEEP_WAIT_S * 1000) { 900 | // Been a really long time, Slowest interval, GPS OFF 901 | tx_time_ms = sleep_time_ms; 902 | need_light_sleep = true; 903 | need_deep_sleep_s = SLEEP_TIME_S; 904 | } else { 905 | // Parked/stationary or battery below REST_LOW_VOLTAGE 906 | need_light_sleep = true; 907 | need_deep_sleep_s = 0; // Keep GPS on 908 | tx_time_ms = rest_time_ms; 909 | } 910 | 911 | // Last, there is User Override! 912 | if (stay_on) { 913 | need_light_sleep = false; 914 | need_deep_sleep_s = 0; 915 | } 916 | 917 | /* Do we send an uplink now? */ 918 | 919 | // Deadzone means we don't send unless asked 920 | if (in_deadzone && !justSendNow) 921 | return false; 922 | 923 | // Don't send any mapper packets for time/distance without GPS fix 924 | if (is_gps_lost) 925 | return false; 926 | 927 | char because = '?'; 928 | if (justSendNow) { 929 | justSendNow = false; 930 | Serial.println("** SEND_NOW"); 931 | because = '>'; 932 | } else if (dist_moved > min_dist_moved) { 933 | Serial.println("** MOVING"); 934 | last_moved_ms = now; 935 | because = 'D'; 936 | } else if (now - last_send_ms > tx_time_ms) { 937 | Serial.println("** TIME"); 938 | because = 'T'; 939 | } else { 940 | return false; // Nothing to do, go home early 941 | } 942 | 943 | appPort = FPORT_MAPPER; 944 | if (!prepare_map_uplink(appPort)) // Don't send bad data 945 | return false; 946 | 947 | // The first distance-moved is crazy, since has no origin.. don't put it on screen. 948 | if (dist_moved > 1000000) 949 | dist_moved = 0; 950 | 951 | printGPSInfo(); 952 | snprintf(buffer, sizeof(buffer), "%d %c %ds %dm\n", UpLinkCounter, because, (now - last_send_ms) / 1000, (int32_t)dist_moved); 953 | // Serial.print(buffer); 954 | screen_print(buffer); 955 | 956 | // Serial.println("send.."); 957 | last_send_ms = now; 958 | LoRaWAN.send(); 959 | // Serial.println("..sent"); 960 | 961 | return true; 962 | } 963 | 964 | void enter_light_sleep(void) { 965 | Serial.println("ENTER light sleep"); 966 | in_light_sleep = true; 967 | TimerStop(&ScreenUpdateTimer); 968 | VextOFF(); // No RGB LED or OLED power 969 | display.stop(); 970 | isDisplayOn = false; 971 | } 972 | 973 | void exit_light_sleep(void) { 974 | Serial.println("EXIT light sleep"); 975 | in_light_sleep = false; 976 | VextON(); 977 | isDisplayOn = true; 978 | display.init(); 979 | screen_setup(); 980 | draw_screen(); 981 | TimerReset(&ScreenUpdateTimer); 982 | TimerStart(&ScreenUpdateTimer); 983 | } 984 | 985 | boolean deep_sleep_wake = false; 986 | uint32_t wake_count = 0; 987 | void onDeepSleepTimer(void) { 988 | // Serial.println("WAKE Time"); 989 | deep_sleep_wake = true; 990 | } 991 | 992 | void deepest_sleep(uint32_t sleepfor_s) { 993 | boolean was_in_light_sleep = in_light_sleep; 994 | 995 | if (!in_light_sleep) 996 | enter_light_sleep(); // Turn off screen 997 | in_deep_sleep = true; 998 | 999 | AirGPS.end(); 1000 | Radio.Sleep(); 1001 | TimerStop(&BatteryUpdateTimer); 1002 | 1003 | Serial.printf("Sleep %d s[\n", sleepfor_s); 1004 | Serial.flush(); 1005 | delay(20); 1006 | 1007 | /* Set a Timer to wake */ 1008 | deep_sleep_wake = false; 1009 | TimerSetValue(&DeepSleepTimer, sleepfor_s * 1000); 1010 | TimerStart(&DeepSleepTimer); 1011 | wake_count = 0; 1012 | wakeByUart = false; 1013 | do { 1014 | lowPowerHandler(); // SLEEP 1015 | wake_count++; 1016 | } while (!deep_sleep_wake && !keyIRQ); 1017 | TimerStop(&DeepSleepTimer); 1018 | Serial.printf("]up; Woke %d times\n", wake_count); 1019 | in_deep_sleep = false; 1020 | 1021 | fast_start_gps(); 1022 | last_fix_ms = 0; 1023 | uint32_t gps_start_time = millis(); 1024 | do { 1025 | update_gps(); 1026 | } while (!last_fix_ms && (millis() - gps_start_time) < SLEEP_GPS_TIMEOUT_S * 1000); 1027 | if (!last_fix_ms) 1028 | Serial.println("(Woke but no Fix)"); 1029 | else { 1030 | Serial.print("Wake fix @ "); 1031 | gps_time(buffer, sizeof(buffer)); 1032 | Serial.println(buffer); 1033 | } 1034 | TimerReset(&BatteryUpdateTimer); 1035 | TimerStart(&BatteryUpdateTimer); 1036 | 1037 | if (!was_in_light_sleep) 1038 | exit_light_sleep(); // Restore screen if it was on. 1039 | } 1040 | 1041 | void onJoinFailTimer(void) { 1042 | screen_print("Join timed out!\n"); 1043 | 1044 | //need_deep_sleep_s = JOIN_RETRY_TIME_S; 1045 | // Now try again 1046 | 1047 | TimerReset(&JoinFailTimer); 1048 | TimerStart(&JoinFailTimer); 1049 | } 1050 | 1051 | void loop() { 1052 | static uint32_t lora_start_time; 1053 | 1054 | // Handle any pending key events 1055 | userKeyIRQ_process(); 1056 | 1057 | if (go_menu_select) { 1058 | go_menu_select = false; 1059 | menu_selected(); 1060 | } 1061 | 1062 | if (need_light_sleep && !in_light_sleep && !in_menu && !hold_screen_on) { 1063 | enter_light_sleep(); 1064 | } 1065 | if (in_light_sleep && (!need_light_sleep || in_menu || hold_screen_on)) { 1066 | exit_light_sleep(); 1067 | } 1068 | if (need_deep_sleep_s && !in_deep_sleep && !in_menu && !hold_screen_on) { 1069 | deepest_sleep(need_deep_sleep_s); 1070 | need_deep_sleep_s = 0; 1071 | } 1072 | 1073 | // Serial.print("."); 1074 | update_gps(); // Digest any pending bytes to update position 1075 | 1076 | switch (deviceState) { 1077 | case DEVICE_STATE_INIT: { 1078 | // Serial.print("[INIT] "); 1079 | lora_start_time = millis(); 1080 | #if (AT_SUPPORT) 1081 | getDevParam(); 1082 | #endif 1083 | printDevParam(); 1084 | LoRaWAN.init(loraWanClass, loraWanRegion); 1085 | LoRaWAN.setDataRateForNoADR(3); // Set DR_3 / SF7 1086 | // deviceState = DEVICE_STATE_JOIN; 1087 | break; 1088 | } 1089 | case DEVICE_STATE_JOIN: { 1090 | // Serial.print("[JOIN] "); 1091 | TimerSetValue(&JoinFailTimer, JOIN_TIMEOUT_S * 1000); 1092 | TimerStart(&JoinFailTimer); 1093 | LoRaWAN.displayJoining(); 1094 | LoRaWAN.join(); 1095 | break; 1096 | } 1097 | case DEVICE_STATE_SEND: { 1098 | // Serial.print("[SEND] "); 1099 | TimerStop(&JoinFailTimer); 1100 | if (!is_joined) { 1101 | is_joined = true; 1102 | snprintf(buffer, sizeof(buffer), "Joined Helium: %d sec\n", (millis() - lora_start_time) / 1000); 1103 | screen_print(buffer); 1104 | justSendNow = true; 1105 | } 1106 | send_uplink(); 1107 | deviceState = DEVICE_STATE_CYCLE; // take a break if we sent or not. 1108 | break; 1109 | } 1110 | case DEVICE_STATE_CYCLE: { 1111 | // Serial.print("[CYCLE] "); 1112 | LoRaWAN.cycle(appTxDutyCycle); // Sets a timer to check state 1113 | deviceState = DEVICE_STATE_SLEEP; 1114 | break; 1115 | } 1116 | case DEVICE_STATE_SLEEP: { 1117 | // Serial.print("[SLEEP] "); 1118 | wakeByUart = true; // Without this, sleeps through GPS 1119 | LoRaWAN.sleep(); // Causes serial port noise if it does sleep 1120 | break; 1121 | } 1122 | default: { 1123 | Serial.printf("Surprising state: %d\n", deviceState); 1124 | deviceState = DEVICE_STATE_INIT; 1125 | break; 1126 | } 1127 | } 1128 | } --------------------------------------------------------------------------------