Initial Prototype Firmware

This commit is contained in:
seiichiro 2021-05-15 19:47:57 +02:00
commit 6917b99156
14 changed files with 1616 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
src/config.h

7
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,7 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
]
}

79
README.md Normal file
View file

@ -0,0 +1,79 @@
# ATTMapper (WIP)
A TTN Mapper based on the ATTNode v3.
**Everything in this repository is work in progress, use at your own risk!**
## Hardware
* Attnode v3 with PCB-Edge SMA Antenna Mount
* Powerpack, LiPo Cell (500mA), LiPo Charger Board (You can use any 3,3V Power Source)
* NEO M8N GPS Breakout (Any 3.3V-Capable Module which is NMEA-Compatible should work)
* SSD1306 128x64 OLED-Display
* Button
### Connections
Connect the AttNode and Modules as follows:
#### GPS
| ATTNode | Module |
|---------|--------|
| Vin | VCC |
| GND | GND |
| Tx | RX |
| Rx | TX |
#### OLED
| ATTNode | Module |
|---------|--------|
| Vin | VCC |
| GND | GND |
| SDA | SDA |
| SCL | SCL |
#### Button
The Button has to be Connected between PC2 and GND on the ATTNode
## Software
This repository contains the PlatformIO project with the mapper firmware. Copy config.example.h to config.h and insert your keys and DeviceID from TTN. Compile and upload as normal (See AttNode v3 dokumentation for details).
### Usage
If a Button is connected you can use the following functions:
* **Short press:** Schedule an imideate send, regardless of interval
* **Long Press (>1s):** Enter Setup Mode, change interval with short press, use another long press to leave setup
## License
Unless stated otherwise, everything in this repository is under the BSD 3-Clause License as follows:
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

39
include/README Normal file
View file

@ -0,0 +1,39 @@
This directory is intended for project header files.
A header file is a file containing C declarations and macro definitions
to be shared between several project source files. You request the use of a
header file in your project source file (C, C++, etc) located in `src` folder
by including it, with the C preprocessing directive `#include'.
```src/main.c
#include "header.h"
int main (void)
{
...
}
```
Including a header file produces the same results as copying the header file
into each source file that needs it. Such copying would be time-consuming
and error-prone. With a header file, the related declarations appear
in only one place. If they need to be changed, they can be changed in one
place, and programs that include the header file will automatically use the
new version when next recompiled. The header file eliminates the labor of
finding and changing all the copies as well as the risk that a failure to
find one copy will result in inconsistencies within a program.
In C, the usual convention is to give header files names that end with `.h'.
It is most portable to use only letters, digits, dashes, and underscores in
header file names, and at most one dot.
Read more about using header files in official GCC documentation:
* Include Syntax
* Include Operation
* Once-Only Headers
* Computed Includes
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html

671
lib/LoRaWAN/LoRaWAN.cpp Normal file
View file

@ -0,0 +1,671 @@
/*
LoRaWAN.cpp - Library for LoRaWAN protocol, uses RFM95W module
Created by Leo Korbee, March 31, 2018.
Released into the public domain.
@license Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
Thanks to all the folks who contributed on the base of this code.
(Gerben den Hartog, et al - Ideetron.nl)
*/
#include "Arduino.h"
#include "LoRaWAN.h"
// constructor
LoRaWAN::LoRaWAN(RFM95 &rfm95)
{
_rfm95 = &rfm95;
}
void LoRaWAN::setKeys(unsigned char NwkSkey[], unsigned char AppSkey[], unsigned char DevAddr[])
{
_NwkSkey = NwkSkey;
_AppSkey = AppSkey;
_DevAddr = DevAddr;
}
/*
*****************************************************************************************
* Description : Function contstructs a LoRaWAN package and sends it
*
* Arguments : *Data pointer to the array of data that will be transmitted
* Data_Length nuber of bytes to be transmitted
* Frame_Counter_Up Frame counter of upstream frames
*
*****************************************************************************************
*/
void LoRaWAN::Send_Data(unsigned char *Data, unsigned char Data_Length, unsigned int Frame_Counter_Tx, lora_dr_t datarate,unsigned char Frame_Port)
{
//Define variables
unsigned char i;
//Direction of frame is up
unsigned char Direction = 0x00;
unsigned char RFM_Data[64];
unsigned char RFM_Package_Length;
unsigned char MIC[4];
/*
@leo:
https://hackmd.io/s/S1kg6Ymo-
75 bits 42 bits 10 bits
MType RFU Major
MType Description
000 (0x00) Join Request
001 (0x20) Join Accept
010 (0x40) Unconfirmed Data Up
011 (0x60) Unconfirmed Data Down
100 (0x80) Confirmed Data Up
101 (0xA0) Confirmed Data Down
110 (0xC0) RFU
111 (0xE0) Proprietary
*/
// Unconfirmed data up
unsigned char Mac_Header = 0x40;
// Confirmed data up
// unsigned char Mac_Header = 0x80;
unsigned char Frame_Control = 0x00;
//unsigned char Frame_Port = 0x01;
//Encrypt the data
Encrypt_Payload(Data, Data_Length, Frame_Counter_Tx, Direction);
//Build the Radio Package
RFM_Data[0] = Mac_Header;
RFM_Data[1] = _DevAddr[3];
RFM_Data[2] = _DevAddr[2];
RFM_Data[3] = _DevAddr[1];
RFM_Data[4] = _DevAddr[0];
RFM_Data[5] = Frame_Control;
RFM_Data[6] = (Frame_Counter_Tx & 0x00FF);
RFM_Data[7] = ((Frame_Counter_Tx >> 8) & 0x00FF);
RFM_Data[8] = Frame_Port;
//Set Current package length
RFM_Package_Length = 9;
//Load Data
for(i = 0; i < Data_Length; i++)
{
RFM_Data[RFM_Package_Length + i] = Data[i];
}
//Add data Lenth to package length
RFM_Package_Length = RFM_Package_Length + Data_Length;
//Calculate MIC
Calculate_MIC(RFM_Data, MIC, RFM_Package_Length, Frame_Counter_Tx, Direction);
//Load MIC in package
for(i = 0; i < 4; i++)
{
RFM_Data[i + RFM_Package_Length] = MIC[i];
}
//Add MIC length to RFM package length
RFM_Package_Length = RFM_Package_Length + 4;
//Set Lora Datarate
_rfm95->RFM_Set_Datarate(datarate);
//Send Package
_rfm95->RFM_Send_Package(RFM_Data, RFM_Package_Length);
}
/*
Encryption stuff after this line
*/
void LoRaWAN::Encrypt_Payload(unsigned char *Data, unsigned char Data_Length, unsigned int Frame_Counter, unsigned char Direction)
{
unsigned char i = 0x00;
unsigned char j;
unsigned char Number_of_Blocks = 0x00;
unsigned char Incomplete_Block_Size = 0x00;
unsigned char Block_A[16];
//Calculate number of blocks
Number_of_Blocks = Data_Length / 16;
Incomplete_Block_Size = Data_Length % 16;
if(Incomplete_Block_Size != 0)
{
Number_of_Blocks++;
}
for(i = 1; i <= Number_of_Blocks; i++)
{
Block_A[0] = 0x01;
Block_A[1] = 0x00;
Block_A[2] = 0x00;
Block_A[3] = 0x00;
Block_A[4] = 0x00;
Block_A[5] = Direction;
Block_A[6] = _DevAddr[3];
Block_A[7] = _DevAddr[2];
Block_A[8] = _DevAddr[1];
Block_A[9] = _DevAddr[0];
Block_A[10] = (Frame_Counter & 0x00FF);
Block_A[11] = ((Frame_Counter >> 8) & 0x00FF);
Block_A[12] = 0x00; //Frame counter upper Bytes
Block_A[13] = 0x00;
Block_A[14] = 0x00;
Block_A[15] = i;
//Calculate S
AES_Encrypt(Block_A, _AppSkey); //original
//Check for last block
if(i != Number_of_Blocks)
{
for(j = 0; j < 16; j++)
{
*Data = *Data ^ Block_A[j];
Data++;
}
}
else
{
if(Incomplete_Block_Size == 0)
{
Incomplete_Block_Size = 16;
}
for(j = 0; j < Incomplete_Block_Size; j++)
{
*Data = *Data ^ Block_A[j];
Data++;
}
}
}
}
void LoRaWAN::Calculate_MIC(unsigned char *Data, unsigned char *Final_MIC, unsigned char Data_Length, unsigned int Frame_Counter, unsigned char Direction)
{
unsigned char i;
unsigned char Block_B[16];
unsigned char Key_K1[16] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
unsigned char Key_K2[16] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
//unsigned char Data_Copy[16];
unsigned char Old_Data[16] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
unsigned char New_Data[16] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
unsigned char Number_of_Blocks = 0x00;
unsigned char Incomplete_Block_Size = 0x00;
unsigned char Block_Counter = 0x01;
//Create Block_B
Block_B[0] = 0x49;
Block_B[1] = 0x00;
Block_B[2] = 0x00;
Block_B[3] = 0x00;
Block_B[4] = 0x00;
Block_B[5] = Direction;
Block_B[6] = _DevAddr[3];
Block_B[7] = _DevAddr[2];
Block_B[8] = _DevAddr[1];
Block_B[9] = _DevAddr[0];
Block_B[10] = (Frame_Counter & 0x00FF);
Block_B[11] = ((Frame_Counter >> 8) & 0x00FF);
Block_B[12] = 0x00; //Frame counter upper bytes
Block_B[13] = 0x00;
Block_B[14] = 0x00;
Block_B[15] = Data_Length;
//Calculate number of Blocks and blocksize of last block
Number_of_Blocks = Data_Length / 16;
Incomplete_Block_Size = Data_Length % 16;
if(Incomplete_Block_Size != 0)
{
Number_of_Blocks++;
}
Generate_Keys(Key_K1, Key_K2);
//Preform Calculation on Block B0
//Preform AES encryption
AES_Encrypt(Block_B, _NwkSkey);
//Copy Block_B to Old_Data
for(i = 0; i < 16; i++)
{
Old_Data[i] = Block_B[i];
}
//Preform full calculating until n-1 messsage blocks
while(Block_Counter < Number_of_Blocks)
{
//Copy data into array
for(i = 0; i < 16; i++)
{
New_Data[i] = *Data;
Data++;
}
//Preform XOR with old data
XOR(New_Data,Old_Data);
//Preform AES encryption
AES_Encrypt(New_Data, _NwkSkey);
//Copy New_Data to Old_Data
for(i = 0; i < 16; i++)
{
Old_Data[i] = New_Data[i];
}
//Raise Block counter
Block_Counter++;
}
//Perform calculation on last block
//Check if Datalength is a multiple of 16
if(Incomplete_Block_Size == 0)
{
//Copy last data into array
for(i = 0; i < 16; i++)
{
New_Data[i] = *Data;
Data++;
}
//Preform XOR with Key 1
XOR(New_Data,Key_K1);
//Preform XOR with old data
XOR(New_Data,Old_Data);
//Preform last AES routine
// read NwkSkey from PROGMEM
AES_Encrypt(New_Data, _NwkSkey);
}
else
{
//Copy the remaining data and fill the rest
for(i = 0; i < 16; i++)
{
if(i < Incomplete_Block_Size)
{
New_Data[i] = *Data;
Data++;
}
if(i == Incomplete_Block_Size)
{
New_Data[i] = 0x80;
}
if(i > Incomplete_Block_Size)
{
New_Data[i] = 0x00;
}
}
//Preform XOR with Key 2
XOR(New_Data,Key_K2);
//Preform XOR with Old data
XOR(New_Data,Old_Data);
//Preform last AES routine
AES_Encrypt(New_Data, _NwkSkey);
}
Final_MIC[0] = New_Data[0];
Final_MIC[1] = New_Data[1];
Final_MIC[2] = New_Data[2];
Final_MIC[3] = New_Data[3];
}
void LoRaWAN::Generate_Keys(unsigned char *K1, unsigned char *K2)
{
unsigned char i;
unsigned char MSB_Key;
//Encrypt the zeros in K1 with the NwkSkey
AES_Encrypt(K1,_NwkSkey);
//Create K1
//Check if MSB is 1
if((K1[0] & 0x80) == 0x80)
{
MSB_Key = 1;
}
else
{
MSB_Key = 0;
}
//Shift K1 one bit left
Shift_Left(K1);
//if MSB was 1
if(MSB_Key == 1)
{
K1[15] = K1[15] ^ 0x87;
}
//Copy K1 to K2
for( i = 0; i < 16; i++)
{
K2[i] = K1[i];
}
//Check if MSB is 1
if((K2[0] & 0x80) == 0x80)
{
MSB_Key = 1;
}
else
{
MSB_Key = 0;
}
//Shift K2 one bit left
Shift_Left(K2);
//Check if MSB was 1
if(MSB_Key == 1)
{
K2[15] = K2[15] ^ 0x87;
}
}
void LoRaWAN::Shift_Left(unsigned char *Data)
{
unsigned char i;
unsigned char Overflow = 0;
//unsigned char High_Byte, Low_Byte;
for(i = 0; i < 16; i++)
{
//Check for overflow on next byte except for the last byte
if(i < 15)
{
//Check if upper bit is one
if((Data[i+1] & 0x80) == 0x80)
{
Overflow = 1;
}
else
{
Overflow = 0;
}
}
else
{
Overflow = 0;
}
//Shift one left
Data[i] = (Data[i] << 1) + Overflow;
}
}
void LoRaWAN::XOR(unsigned char *New_Data,unsigned char *Old_Data)
{
unsigned char i;
for(i = 0; i < 16; i++)
{
New_Data[i] = New_Data[i] ^ Old_Data[i];
}
}
/*
*****************************************************************************************
* Title : AES_Encrypt
* Description :
*****************************************************************************************
*/
void LoRaWAN::AES_Encrypt(unsigned char *Data, unsigned char *Key)
{
unsigned char Row, Column, Round = 0;
unsigned char Round_Key[16];
unsigned char State[4][4];
// Copy input to State arry
for( Column = 0; Column < 4; Column++ )
{
for( Row = 0; Row < 4; Row++ )
{
State[Row][Column] = Data[Row + (Column << 2)];
}
}
// Copy key to round key
memcpy( &Round_Key[0], &Key[0], 16 );
// Add round key
AES_Add_Round_Key( Round_Key, State );
// Preform 9 full rounds with mixed collums
for( Round = 1 ; Round < 10 ; Round++ )
{
// Perform Byte substitution with S table
for( Column = 0 ; Column < 4 ; Column++ )
{
for( Row = 0 ; Row < 4 ; Row++ )
{
State[Row][Column] = AES_Sub_Byte( State[Row][Column] );
}
}
// Perform Row Shift
AES_Shift_Rows(State);
// Mix Collums
AES_Mix_Collums(State);
// Calculate new round key
AES_Calculate_Round_Key(Round, Round_Key);
// Add the round key to the Round_key
AES_Add_Round_Key(Round_Key, State);
}
// Perform Byte substitution with S table whitout mix collums
for( Column = 0 ; Column < 4 ; Column++ )
{
for( Row = 0; Row < 4; Row++ )
{
State[Row][Column] = AES_Sub_Byte(State[Row][Column]);
}
}
// Shift rows
AES_Shift_Rows(State);
// Calculate new round key
AES_Calculate_Round_Key( Round, Round_Key );
// Add round key
AES_Add_Round_Key( Round_Key, State );
// Copy the State into the data array
for( Column = 0; Column < 4; Column++ )
{
for( Row = 0; Row < 4; Row++ )
{
Data[Row + (Column << 2)] = State[Row][Column];
}
}
} // AES_Encrypt
/*
*****************************************************************************************
* Title : AES_Add_Round_Key
* Description :
*****************************************************************************************
*/
void LoRaWAN::AES_Add_Round_Key(unsigned char *Round_Key, unsigned char (*State)[4])
{
unsigned char Row, Collum;
for(Collum = 0; Collum < 4; Collum++)
{
for(Row = 0; Row < 4; Row++)
{
State[Row][Collum] ^= Round_Key[Row + (Collum << 2)];
}
}
} // AES_Add_Round_Key
/*
*****************************************************************************************
* Title : AES_Sub_Byte
* Description :
*****************************************************************************************
*/
unsigned char LoRaWAN::AES_Sub_Byte(unsigned char Byte)
{
// unsigned char S_Row,S_Collum;
// unsigned char S_Byte;
//
// S_Row = ((Byte >> 4) & 0x0F);
// S_Collum = ((Byte >> 0) & 0x0F);
// S_Byte = S_Table [S_Row][S_Collum];
//return S_Table [ ((Byte >> 4) & 0x0F) ] [ ((Byte >> 0) & 0x0F) ]; // original
return pgm_read_byte(&(S_Table [((Byte >> 4) & 0x0F)] [((Byte >> 0) & 0x0F)]));
} // AES_Sub_Byte
/*
*****************************************************************************************
* Title : AES_Shift_Rows
* Description :
*****************************************************************************************
*/
void LoRaWAN::AES_Shift_Rows(unsigned char (*State)[4])
{
unsigned char Buffer;
//Store firt byte in buffer
Buffer = State[1][0];
//Shift all bytes
State[1][0] = State[1][1];
State[1][1] = State[1][2];
State[1][2] = State[1][3];
State[1][3] = Buffer;
Buffer = State[2][0];
State[2][0] = State[2][2];
State[2][2] = Buffer;
Buffer = State[2][1];
State[2][1] = State[2][3];
State[2][3] = Buffer;
Buffer = State[3][3];
State[3][3] = State[3][2];
State[3][2] = State[3][1];
State[3][1] = State[3][0];
State[3][0] = Buffer;
} // AES_Shift_Rows
/*
*****************************************************************************************
* Title : AES_Mix_Collums
* Description :
*****************************************************************************************
*/
void LoRaWAN::AES_Mix_Collums(unsigned char (*State)[4])
{
unsigned char Row,Collum;
unsigned char a[4], b[4];
for(Collum = 0; Collum < 4; Collum++)
{
for(Row = 0; Row < 4; Row++)
{
a[Row] = State[Row][Collum];
b[Row] = (State[Row][Collum] << 1);
if((State[Row][Collum] & 0x80) == 0x80)
{
b[Row] ^= 0x1B;
}
}
State[0][Collum] = b[0] ^ a[1] ^ b[1] ^ a[2] ^ a[3];
State[1][Collum] = a[0] ^ b[1] ^ a[2] ^ b[2] ^ a[3];
State[2][Collum] = a[0] ^ a[1] ^ b[2] ^ a[3] ^ b[3];
State[3][Collum] = a[0] ^ b[0] ^ a[1] ^ a[2] ^ b[3];
}
} // AES_Mix_Collums
/*
*****************************************************************************************
* Title : AES_Calculate_Round_Key
* Description :
*****************************************************************************************
*/
void LoRaWAN::AES_Calculate_Round_Key(unsigned char Round, unsigned char *Round_Key)
{
unsigned char i, j, b, Rcon;
unsigned char Temp[4];
//Calculate Rcon
Rcon = 0x01;
while(Round != 1)
{
b = Rcon & 0x80;
Rcon = Rcon << 1;
if(b == 0x80)
{
Rcon ^= 0x1b;
}
Round--;
}
// Calculate first Temp
// Copy laste byte from previous key and subsitute the byte, but shift the array contents around by 1.
Temp[0] = AES_Sub_Byte( Round_Key[12 + 1] );
Temp[1] = AES_Sub_Byte( Round_Key[12 + 2] );
Temp[2] = AES_Sub_Byte( Round_Key[12 + 3] );
Temp[3] = AES_Sub_Byte( Round_Key[12 + 0] );
// XOR with Rcon
Temp[0] ^= Rcon;
// Calculate new key
for(i = 0; i < 4; i++)
{
for(j = 0; j < 4; j++)
{
Round_Key[j + (i << 2)] ^= Temp[j];
Temp[j] = Round_Key[j + (i << 2)];
}
}
} // AES_Calculate_Round_Key

66
lib/LoRaWAN/LoRaWAN.h Normal file
View file

@ -0,0 +1,66 @@
/*
LoRaWAN.h - Library header file for LoRaWAN protocol, uses RFM95W module
Created by Leo Korbee, March 31, 2018.
Released into the public domain.
@license Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
Thanks to all the folks who contributed before me on this code.
*/
#include "../RFM95/RFM95.h"
#include "Arduino.h"
#ifndef LoRaWAN_h
#define LoRaWAN_h
// for AES encryption
static const unsigned char PROGMEM S_Table[16][16] = {
{0x63,0x7C,0x77,0x7B,0xF2,0x6B,0x6F,0xC5,0x30,0x01,0x67,0x2B,0xFE,0xD7,0xAB,0x76},
{0xCA,0x82,0xC9,0x7D,0xFA,0x59,0x47,0xF0,0xAD,0xD4,0xA2,0xAF,0x9C,0xA4,0x72,0xC0},
{0xB7,0xFD,0x93,0x26,0x36,0x3F,0xF7,0xCC,0x34,0xA5,0xE5,0xF1,0x71,0xD8,0x31,0x15},
{0x04,0xC7,0x23,0xC3,0x18,0x96,0x05,0x9A,0x07,0x12,0x80,0xE2,0xEB,0x27,0xB2,0x75},
{0x09,0x83,0x2C,0x1A,0x1B,0x6E,0x5A,0xA0,0x52,0x3B,0xD6,0xB3,0x29,0xE3,0x2F,0x84},
{0x53,0xD1,0x00,0xED,0x20,0xFC,0xB1,0x5B,0x6A,0xCB,0xBE,0x39,0x4A,0x4C,0x58,0xCF},
{0xD0,0xEF,0xAA,0xFB,0x43,0x4D,0x33,0x85,0x45,0xF9,0x02,0x7F,0x50,0x3C,0x9F,0xA8},
{0x51,0xA3,0x40,0x8F,0x92,0x9D,0x38,0xF5,0xBC,0xB6,0xDA,0x21,0x10,0xFF,0xF3,0xD2},
{0xCD,0x0C,0x13,0xEC,0x5F,0x97,0x44,0x17,0xC4,0xA7,0x7E,0x3D,0x64,0x5D,0x19,0x73},
{0x60,0x81,0x4F,0xDC,0x22,0x2A,0x90,0x88,0x46,0xEE,0xB8,0x14,0xDE,0x5E,0x0B,0xDB},
{0xE0,0x32,0x3A,0x0A,0x49,0x06,0x24,0x5C,0xC2,0xD3,0xAC,0x62,0x91,0x95,0xE4,0x79},
{0xE7,0xC8,0x37,0x6D,0x8D,0xD5,0x4E,0xA9,0x6C,0x56,0xF4,0xEA,0x65,0x7A,0xAE,0x08},
{0xBA,0x78,0x25,0x2E,0x1C,0xA6,0xB4,0xC6,0xE8,0xDD,0x74,0x1F,0x4B,0xBD,0x8B,0x8A},
{0x70,0x3E,0xB5,0x66,0x48,0x03,0xF6,0x0E,0x61,0x35,0x57,0xB9,0x86,0xC1,0x1D,0x9E},
{0xE1,0xF8,0x98,0x11,0x69,0xD9,0x8E,0x94,0x9B,0x1E,0x87,0xE9,0xCE,0x55,0x28,0xDF},
{0x8C,0xA1,0x89,0x0D,0xBF,0xE6,0x42,0x68,0x41,0x99,0x2D,0x0F,0xB0,0x54,0xBB,0x16}
};
class LoRaWAN
{
public:
LoRaWAN(RFM95 &rfm95);
void setKeys(unsigned char NwkSkey[], unsigned char AppSkey[], unsigned char DevAddr[]);
void Send_Data(unsigned char *Data, unsigned char Data_Length, unsigned int Frame_Counter_Tx, lora_dr_t datarate,unsigned char Frame_Port);
private:
RFM95 *_rfm95;
// remember arrays are pointers!
unsigned char *_NwkSkey;
unsigned char *_AppSkey;
unsigned char *_DevAddr;
void RFM_Send_Package(unsigned char *RFM_Tx_Package, unsigned char Package_Length);
// security stuff:
void Encrypt_Payload(unsigned char *Data, unsigned char Data_Length, unsigned int Frame_Counter, unsigned char Direction);
void Calculate_MIC(unsigned char *Data, unsigned char *Final_MIC, unsigned char Data_Length, unsigned int Frame_Counter, unsigned char Direction);
void Generate_Keys(unsigned char *K1, unsigned char *K2);
void Shift_Left(unsigned char *Data);
void XOR(unsigned char *New_Data,unsigned char *Old_Data);
void AES_Encrypt(unsigned char *Data, unsigned char *Key);
void AES_Add_Round_Key(unsigned char *Round_Key, unsigned char (*State)[4]);
unsigned char AES_Sub_Byte(unsigned char Byte);
void AES_Shift_Rows(unsigned char (*State)[4]);
void AES_Mix_Collums(unsigned char (*State)[4]);
void AES_Calculate_Round_Key(unsigned char Round, unsigned char *Round_Key);
};
#endif

46
lib/README Normal file
View file

@ -0,0 +1,46 @@
This directory is intended for project specific (private) libraries.
PlatformIO will compile them to static libraries and link into executable file.
The source code of each library should be placed in a an own separate directory
("lib/your_library_name/[here are source files]").
For example, see a structure of the following two libraries `Foo` and `Bar`:
|--lib
| |
| |--Bar
| | |--docs
| | |--examples
| | |--src
| | |- Bar.c
| | |- Bar.h
| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
| |
| |--Foo
| | |- Foo.c
| | |- Foo.h
| |
| |- README --> THIS FILE
|
|- platformio.ini
|--src
|- main.c
and a contents of `src/main.c`:
```
#include <Foo.h>
#include <Bar.h>
int main (void)
{
...
}
```
PlatformIO Library Dependency Finder will find automatically dependent
libraries scanning project source files.
More information about PlatformIO Library Dependency Finder
- https://docs.platformio.org/page/librarymanager/ldf.html

333
lib/RFM95/RFM95.cpp Normal file
View file

@ -0,0 +1,333 @@
/*
RFM95.cpp - Library for RFM95 LoRa module.
Created by Leo Korbee, March 31, 2018.
Released into the public domain.
@license Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
Thanks to all the folks who contributed on the base of this code.
(Gerben den Hartog, et al - Ideetron.nl)
*/
#include "Arduino.h"
#include "RFM95.h"
#include <SPI.h>
// constructor
RFM95::RFM95(int DIO0, int NSS)
{
_DIO0 = DIO0;
_NSS = NSS;
// init tinySPI
SPI.setDataMode(SPI_MODE0);
SPI.begin();
}
/*
*****************************************************************************************
* Description: Function used to initialize the RFM module on startup
*****************************************************************************************
*/
void RFM95::init()
{
// set pinmodes input/output
pinMode(_NSS, OUTPUT);
pinMode(_DIO0, INPUT);
// Set default Datarate Config SF7BW125
_sf = 0x74;
_bw = 0x72;
_mc = 0x04;
// NSS for starting and stopping communication with the RFM95 module
digitalWrite(_NSS, HIGH);
//Switch RFM to sleep
RFM_Write(0x01,0x00);
//Set RFM in LoRa mode
RFM_Write(0x01,0x80);
//Set RFM in Standby mode wait on mode ready
RFM_Write(0x01,0x81);
/*
while (digitalRead(DIO5) == LOW)
{
}
*/
delay(10);
//Set carrair frequency
// 868.100 MHz / 61.035 Hz = 14222987 = 0xD9068B
RFM_Write(0x06,0xD9);
RFM_Write(0x07,0x06);
RFM_Write(0x08,0x8B);
// Minmal Power
//RFM_Write(0x09,0xF0);
//PA pin (maximal power)
RFM_Write(0x09,0xFF);
//BW = 125 kHz, Coding rate 4/5, Explicit header mode
RFM_Write(0x1D,0x72);
//Spreading factor 7, PayloadCRC On
RFM_Write(0x1E,0xB4);
//Rx Timeout set to 37 symbols
RFM_Write(0x1F,0x25);
//Preamble length set to 8 symbols
//0x0008 + 4 = 12
RFM_Write(0x20,0x00);
RFM_Write(0x21,0x08);
//Low datarate optimization off AGC auto on
RFM_Write(0x26,0x0C);
//Set LoRa sync word
RFM_Write(0x39,0x34);
//Set IQ to normal values
RFM_Write(0x33,0x27);
RFM_Write(0x3B,0x1D);
//Set FIFO pointers
//TX base adress
RFM_Write(0x0E,0x80);
//Rx base adress
RFM_Write(0x0F,0x00);
//Switch RFM to sleep
RFM_Write(0x01,0x00);
}
/*
*****************************************************************************************
* Description : Funtion that writes a register from the RFM
*
* Arguments : RFM_Address Address of register to be written
* RFM_Data Data to be written
*****************************************************************************************
*/
void RFM95::RFM_Write(unsigned char RFM_Address, unsigned char RFM_Data)
{
//Set NSS pin Low to start communication
digitalWrite(_NSS,LOW);
//Send Addres with MSB 1 to make it a write command
SPI.transfer(RFM_Address | 0x80);
//Send Data
SPI.transfer(RFM_Data);
//Set NSS pin High to end communication
digitalWrite(_NSS,HIGH);
}
/*
*****************************************************************************************
* Description : Funtion that reads a register from the RFM and returns the value
*
* Arguments : RFM_Address Address of register to be read
*
* Returns : Value of the register
*****************************************************************************************
*/
unsigned char RFM95::RFM_Read(unsigned char RFM_Address)
{
unsigned char RFM_Data;
//Set NSS pin low to start SPI communication
digitalWrite(_NSS,LOW);
//Send Address
SPI.transfer(RFM_Address);
//Send 0x00 to be able to receive the answer from the RFM
RFM_Data = SPI.transfer(0x00);
//Set NSS high to end communication
digitalWrite(_NSS,HIGH);
//Return received data
return RFM_Data;
}
/*
*****************************************************************************************
* Description : Set Datarate and Spreading Factor
*
* Arguments : datarate Lora Datarate Enum (see RFM95.h)
*****************************************************************************************
*/
void RFM95::RFM_Set_Datarate(lora_dr_t datarate) {
switch(datarate) {
case SF7BW125:
_sf = 0x74;
_bw = 0x72;
_mc = 0x04;
break;
case SF8BW125:
_sf = 0x84;
_bw = 0x72;
_mc = 0x04;
break;
case SF9BW125:
_sf = 0x94;
_bw = 0x72;
_mc = 0x04;
break;
case SF10BW125:
_sf = 0xA4;
_bw = 0x72;
_mc = 0x04;
break;
case SF11BW125:
_sf = 0xB4;
_bw = 0x72;
_mc = 0x0C;
break;
case SF12BW125:
_sf = 0xC4;
_bw = 0x72;
_mc = 0x0C;
break;
default:
_sf = 0x74;
_bw = 0x72;
_mc = 0x04;
break;
}
}
/*
*****************************************************************************************
* Description : Function for sending a package with the RFM
*
* Arguments : *RFM_Tx_Package Pointer to arry with data to be send
* Package_Length Length of the package to send
*****************************************************************************************
*/
void RFM95::RFM_Send_Package(unsigned char *RFM_Tx_Package, unsigned char Package_Length)
{
unsigned char i;
// unsigned char RFM_Tx_Location = 0x00;
//Set RFM in Standby mode wait on mode ready
RFM_Write(0x01,0x81);
/*
while (digitalRead(DIO5) == LOW)
{
}
*/
delay(10);
//Switch DIO0 to TxDone
//RFM_Write(0x40,0x40);
//Set carrier frequency
/*
fixed frequency
// 868.100 MHz / 61.035 Hz = 14222987 = 0xD9068B
_rfm95.RFM_Write(0x06,0xD9);
_rfm95.RFM_Write(0x07,0x06);
_rfm95.RFM_Write(0x08,0x8B);
*/
//Channel 0 868.100 MHz / 61.035 Hz = 14222987 = 0xD9068B
RFM_Write(0x06,0xD9);
RFM_Write(0x07,0x06);
RFM_Write(0x08,0x8B);
// EU863-870 specifications
/*
// TCNT0 is timer0 continous timer, kind of random selection of frequency
switch (TCNT0 % 8)
{
case 0x00: //Channel 0 868.100 MHz / 61.035 Hz = 14222987 = 0xD9068B
RFM_Write(0x06,0xD9);
RFM_Write(0x07,0x06);
RFM_Write(0x08,0x8B);
break;
case 0x01: //Channel 1 868.300 MHz / 61.035 Hz = 14226264 = 0xD91358
RFM_Write(0x06,0xD9);
RFM_Write(0x07,0x13);
RFM_Write(0x08,0x58);
break;
case 0x02: //Channel 2 868.500 MHz / 61.035 Hz = 14229540 = 0xD92024
RFM_Write(0x06,0xD9);
RFM_Write(0x07,0x20);
RFM_Write(0x08,0x24);
break;
// added five more channels
case 0x03: // Channel 3 867.100 MHz / 61.035 Hz = 14206603 = 0xD8C68B
RFM_Write(0x06,0xD8);
RFM_Write(0x07,0xC6);
RFM_Write(0x08,0x8B);
break;
case 0x04: // Channel 4 867.300 MHz / 61.035 Hz = 14209880 = 0xD8D358
RFM_Write(0x06,0xD8);
RFM_Write(0x07,0xD3);
RFM_Write(0x08,0x58);
break;
case 0x05: // Channel 5 867.500 MHz / 61.035 Hz = 14213156 = 0xD8E024
RFM_Write(0x06,0xD8);
RFM_Write(0x07,0xE0);
RFM_Write(0x08,0x24);
break;
case 0x06: // Channel 6 867.700 MHz / 61.035 Hz = 14216433 = 0xD8ECF1
RFM_Write(0x06,0xD8);
RFM_Write(0x07,0xEC);
RFM_Write(0x08,0xF1);
break;
case 0x07: // Channel 7 867.900 MHz / 61.035 Hz = 14219710 = 0xD8F9BE
RFM_Write(0x06,0xD8);
RFM_Write(0x07,0xF9);
RFM_Write(0x08,0xBE);
break;
// FSK 868.800 Mhz => not used in this config
// 869.525 - SF9BW125 (RX2 downlink only) for package received
}
*/
//SF7 BW 125 kHz
RFM_Write(0x1E,_sf); //SF7 CRC On
RFM_Write(0x1D,_bw); //125 kHz 4/5 coding rate explicit header mode
RFM_Write(0x26,_mc); //Low datarate optimization off AGC auto on
//Set IQ to normal values
RFM_Write(0x33,0x27);
RFM_Write(0x3B,0x1D);
//Set payload length to the right length
RFM_Write(0x22,Package_Length);
//Get location of Tx part of FiFo
//RFM_Tx_Location = RFM_Read(0x0E);
//Set SPI pointer to start of Tx part in FiFo
//RFM_Write(0x0D,RFM_Tx_Location);
RFM_Write(0x0D,0x80); // hardcoded fifo location according RFM95 specs
//Write Payload to FiFo
for (i = 0;i < Package_Length; i++)
{
RFM_Write(0x00,*RFM_Tx_Package);
RFM_Tx_Package++;
}
//Switch RFM to Tx
RFM_Write(0x01,0x83);
//Wait for TxDone
//while( digitalRead(_DIO0) == LOW )
//{
//}
while((RFM_Read(0x12) & 0x08) != 0x08)
{
}
//Clear interrupt
RFM_Write(0x12,0x08);
//Switch RFM to sleep
RFM_Write(0x01,0x00);
}

37
lib/RFM95/RFM95.h Normal file
View file

@ -0,0 +1,37 @@
/*
RFM95.h - Library header file for RFM95 LoRa module.
Created by Leo Korbee, March 31, 2018.
Released into the public domain.
@license Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
Thanks to all the folks who contributed beforme me on this code.
*/
#ifndef RFM95_h
#define RMF95_h
#include "Arduino.h"
typedef enum lora_dr
{
SF7BW125,
SF8BW125,
SF9BW125,
SF10BW125,
SF11BW125,
SF12BW125,
} lora_dr_t;
class RFM95
{
public:
RFM95(int DIO0, int NSS);
void init();
void RFM_Write(unsigned char RFM_Address, unsigned char RFM_Data);
unsigned char RFM_Read(unsigned char RFM_Address);
void RFM_Send_Package(unsigned char *RFM_Tx_Package, unsigned char Package_Length);
void RFM_Set_Datarate(lora_dr_t datarate);
private:
int _DIO0;
int _NSS;
unsigned char _sf, _bw, _mc;
};
#endif

78
lib/gps/gps.cpp Normal file
View file

@ -0,0 +1,78 @@
#include <gps.h>
byte CFG_RST[12] = {0xb5, 0x62, 0x06, 0x04, 0x04, 0x00, 0x00, 0x00, 0x01,0x00, 0x0F, 0x66};
void Gps::init()
{
Serial.begin(9600);
Serial.setTimeout(2);
Serial.write(CFG_RST, sizeof(CFG_RST)); // Soft Reset GPS on Start
satsInView.begin(tGps, "GPGSV", 3); // NMEA Sentence GPGSV, Element 3 (Sat in View)
}
void Gps::encode()
{
int data;
unsigned long previousMillis = millis();
while((previousMillis + 100) > millis())
{
while (Serial.available() )
{
char data = Serial.read();
tGps.encode(data);
}
}
//Serial.println("");
}
bool Gps::buildPacket(uint8_t txBuffer[9])
{
LatitudeBinary = ((tGps.location.lat() + 90) / 180.0) * 16777215;
LongitudeBinary = ((tGps.location.lng() + 180) / 360.0) * 16777215;
txBuffer[0] = ( LatitudeBinary >> 16 ) & 0xFF;
txBuffer[1] = ( LatitudeBinary >> 8 ) & 0xFF;
txBuffer[2] = LatitudeBinary & 0xFF;
txBuffer[3] = ( LongitudeBinary >> 16 ) & 0xFF;
txBuffer[4] = ( LongitudeBinary >> 8 ) & 0xFF;
txBuffer[5] = LongitudeBinary & 0xFF;
altitudeGps = tGps.altitude.meters();
txBuffer[6] = ( altitudeGps >> 8 ) & 0xFF;
txBuffer[7] = altitudeGps & 0xFF;
hdopGps = tGps.hdop.value()/10;
txBuffer[8] = hdopGps & 0xFF;
return true;
}
void Gps::gdisplay(uint16_t dispBuffer[])
{
dispBuffer[0] = tGps.hdop.value()/10;
dispBuffer[1] = TinyGPSPlus::parseDecimal(satsInView.value());
dispBuffer[2] = tGps.satellites.value();
dispBuffer[3] = tGps.altitude.meters();
dispBuffer[4] = tGps.speed.kmph();
}
bool Gps::checkGpsFix()
{
encode();
if (tGps.location.isValid() &&
tGps.location.age() < 4000 &&
tGps.hdop.isValid() &&
tGps.hdop.value() <= 600 &&
tGps.hdop.age() < 4000 &&
tGps.altitude.isValid() &&
tGps.altitude.age() < 4000 )
{
return true;
}
else
{
return false;
}
}

23
lib/gps/gps.h Normal file
View file

@ -0,0 +1,23 @@
#ifndef __GPS_H__
#define __GPS_H__
#include <TinyGPS++.h>
class Gps
{
public:
void init();
bool checkGpsFix();
bool buildPacket(uint8_t txBuffer[]);
void gdisplay(uint16_t dispBuffer[]);
void encode();
private:
uint32_t LatitudeBinary, LongitudeBinary;
uint16_t altitudeGps;
uint8_t hdopGps;
TinyGPSPlus tGps;
TinyGPSCustom satsInView;
};
#endif

48
platformio.ini Normal file
View file

@ -0,0 +1,48 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:ATtiny3216]
platform = atmelmegaavr
board = ATtiny3216
framework = arduino
## Board Config ##
# You might want to set f_cpu to 5MHz (5000000L) to allow operation at lower Battery Voltage - Use "Burn Fuses" after changing f_cpu
# Be aware that some Functions (like WS2812B LED Support) will not work at 5 HMz
board_build.f_cpu = 20000000L
board_hardware.oscillator = internal
board_hardware.bod = disabled
## Debug Port Config ##
monitor_speed = 115200
monitor_port = /dev/ttyACM1
## LMIC Config via Build Flags ##
build_flags =
-D CFG_eu868
-D CFG_sx1276_radio
-D DISABLE_PING
-D DISABLE_BEACONS
-D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS
-D DISABLE_JOIN
## Programmer Config (MicroUPDI) ##
upload_port = usb
upload_protocol = xplainedmini_updi
upload_flags =
-p$BOARD_MCU
-P$UPLOAD_PORT
-c$UPLOAD_PROTOCOL
lib_deps =
mcci-catena/MCCI LoRaWAN LMIC library @ ^3.3.0
mikalhart/TinyGPSPlus @ ^1.0.2
olikraus/U8g2 @ ^2.28.8
lennarthennigs/Button2 @ ^1.6.1

6
src/config.example.h Normal file
View file

@ -0,0 +1,6 @@
//*******************************************************************
// LoRa Config - Put Your Keys Here!
//*******************************************************************
unsigned char NwkSkey[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
unsigned char AppSkey[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
unsigned char DevAddr[4] = { 0x00, 0x00, 0x00, 0x00 };

177
src/main.cpp Normal file
View file

@ -0,0 +1,177 @@
#include <Arduino.h>
#include <avr/sleep.h>
#include <avr/interrupt.h>
#include <SPI.h>
#include <Wire.h>
#include <LoRaWAN.h>
// OLED
#include <U8x8lib.h>
U8X8_SSD1306_128X64_NONAME_HW_I2C oled(U8X8_PIN_NONE, PIN_PB0, PIN_PB1);
// GPS
#include <gps.h>
Gps gps;
bool hasFix = false;
bool oldFix = false;
// Button Handling
#include <Button2.h>
Button2 btn = Button2(PIN_PC2);
bool adhocsend = false;
// Modes for Display Menus and Navigation
enum Modes {
M_NORMAL,
M_SETUP
};
Modes amode = M_NORMAL; // Active Mode
Modes omode = M_NORMAL; // Previous Mode
// LoRa
#include "config.h" // Contains LoRa ABP Keys
#define DIO0 PIN_PA4
#define NSS PIN_PA5
RFM95 rfm(DIO0, NSS);
LoRaWAN lora = LoRaWAN(rfm);
uint16_t Frame_Counter_Tx = 0x0000;
// Some Status Variables
uint8_t interval = 20; // Sending Interval in Settings
uint8_t packets = 0; // Sent LoRa Packets
String statusmsg = "";
uint8_t loraBuffer[9]; // Lora Data Packet
uint32_t lastmillis;
// Button Handler Function
void handler(Button2& btn) {
switch (btn.getClickType()) {
case SINGLE_CLICK:
if (amode == M_SETUP) {
interval = interval += 10;
if (interval > 40)
interval = 10;
} else if (amode == M_NORMAL) {
statusmsg = "Adhoc Request";
adhocsend = true;
}
break;
case LONG_CLICK:
switch (amode) {
case M_NORMAL:
amode = M_SETUP;
break;
case M_SETUP:
amode = M_NORMAL;
break;
}
break;
}
}
// Draw the Display
void updateDisplay() {
uint16_t buffer[6];
gps.gdisplay(buffer);
oled.home();
oled.setFont(u8x8_font_victoriabold8_r);
if (amode != omode) {
oled.clear();
omode = amode;
}
if (amode == M_SETUP) {
oled.println ("<< SETUP >>");
oled.println ("");
oled.print("Interval: ");
oled.println(interval);
} else if (amode == M_NORMAL) {
if (hasFix) {
if (hasFix != oldFix){
oled.clear();
oldFix = hasFix;
}
oled.print("HDOP: ");
oled.println(buffer[0]);
oled.print("Sats: ");
oled.print(buffer[1]);
oled.print("/");
oled.println(buffer[2]);
oled.print("Int: ");
oled.println(interval);
oled.print("Packet: ");
oled.println(Frame_Counter_Tx);
oled.print("Alt: ");
oled.println(buffer[3]);
oled.print("Speed: ");
oled.println(buffer[4]);
} else {
if (hasFix != oldFix){
oled.clear();
oldFix = hasFix;
}
oled.println("NO GPS FIX");
oled.print("Sats: ");
oled.print(buffer[1]);
oled.print("/");
oled.println(buffer[2]);
oled.print("Int: ");
oled.println(interval);
oled.print("Packet: ");
oled.println(Frame_Counter_Tx);
}
oled.print(statusmsg);
oled.println(" ");
}
}
// Setup
void setup() {
// GPS Setup
Serial.begin(9600);
// Button
btn.setLongClickTime(1000);
btn.setClickHandler(handler);
btn.setLongClickHandler(handler);
// OLED Setup
Wire.begin();
Wire.setClock(400000L);
oled.begin();
oled.setPowerSave(0);
oled.setFlipMode(1);
oled.clear();
// Lora Initialization
rfm.init();
lora.setKeys(NwkSkey, AppSkey, DevAddr);
}
// Main Loop
void loop() {
hasFix = gps.checkGpsFix();
uint32_t curmillis = millis();
if (hasFix && (((uint32_t)(curmillis - lastmillis) >= interval*1000) || adhocsend)) {
if (adhocsend) {
statusmsg = "Sending (Adhoc)";
} else {
statusmsg = "Sending";
}
updateDisplay();
adhocsend = false;
gps.buildPacket(loraBuffer);
lora.Send_Data((unsigned char *)&loraBuffer, sizeof(loraBuffer), Frame_Counter_Tx, SF7BW125, 0x01);
Frame_Counter_Tx++;
lastmillis = millis();
} else {
updateDisplay();
}
statusmsg = "";
btn.loop();
}