kopia lustrzana https://github.com/Max-Plastix/tbeam-helium-mapper
974 wiersze
30 KiB
C++
974 wiersze
30 KiB
C++
/*
|
|
Helium Mapper build for LilyGo TTGO T-Beam v0.7, v1.0, and v1.1 boards.
|
|
Copyright (C) 2021 by Max-Plastix
|
|
|
|
This is a development fork by Max-Plastix hosted here:
|
|
https://github.com/Max-Plastix/tbeam-helium-mapper/
|
|
|
|
This code comes from a number of developers and earlier efforts, visible in the
|
|
lineage on Github and prior comments below. GPL makes this all possible --
|
|
continue to modify, extend, and share!
|
|
*/
|
|
|
|
/*
|
|
This module and those attached with it have been modified for the Helium
|
|
Network by Fizzy. The following has been changed from the original
|
|
modifications for Helium, by longfi-arduino:
|
|
- Added Helium Startup Logo
|
|
- Changed App Name and Version of device to reflect more of a device name and
|
|
number scheme.
|
|
- Enabled long press middle button to Discard Prefs by default for future
|
|
troubleshooting on device.
|
|
- Changed Text output to reflect Helium, and not TTL (Code referances ttn,
|
|
just to prevent brakes in this awesome code)
|
|
- Changed credentials file to use OTAA by default.
|
|
- Changed GPS metric output text "Error", to "Accuracy/HDOP".
|
|
*/
|
|
/*
|
|
Main module
|
|
|
|
# Modified by Kyle T. Gabriel to fix issue with incorrect GPS data for
|
|
TTNMapper
|
|
|
|
Copyright (C) 2018 by Xose Pérez <xose dot perez at gmail dot com>
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <Arduino.h>
|
|
#include <Preferences.h>
|
|
#include <Wire.h>
|
|
#include <axp20x.h>
|
|
#include <lmic.h>
|
|
|
|
#include "configuration.h"
|
|
#include "gps.h"
|
|
#include "screen.h"
|
|
#include "sleep.h"
|
|
#include "ttn.h"
|
|
|
|
// Defined in ttn.ino
|
|
void ttn_register(void (*callback)(uint8_t message));
|
|
|
|
bool justSendNow = true; // Start by sending
|
|
unsigned long int last_send_millis = 0;
|
|
unsigned long int last_moved_millis = 0;
|
|
float last_send_lat = 0;
|
|
float last_send_lon = 0;
|
|
float dist_moved = 0;
|
|
|
|
// Deadzone (no uplink) location and radius
|
|
float deadzone_lat = DEADZONE_LAT;
|
|
float deadzone_lon = DEADZONE_LON;
|
|
float deadzone_radius_m = DEADZONE_RADIUS_M;
|
|
boolean in_deadzone = false;
|
|
|
|
/* Defaults that can be overwritten by downlink messages */
|
|
/* 32-bit int seconds is 50 days maximum */
|
|
unsigned int rest_wait_s; // prefs REST_WAIT
|
|
unsigned int rest_tx_interval_s; // prefs REST_TX_INTERVAL
|
|
unsigned int stationary_tx_interval_s; // prefs STATIONARY_TX_INTERVAL
|
|
unsigned int tx_interval_s;
|
|
|
|
float battery_low_voltage = BATTERY_LOW_VOLTAGE;
|
|
float min_dist_moved = MIN_DIST;
|
|
|
|
AXP20X_Class axp;
|
|
bool pmu_irq = false;
|
|
|
|
bool ssd1306_found = false;
|
|
bool axp192_found = false;
|
|
|
|
// bool packetSent;
|
|
bool packetQueued;
|
|
bool isJoined = false;
|
|
|
|
// Buffer for Payload frame
|
|
static uint8_t txBuffer[11];
|
|
|
|
// deep sleep support
|
|
RTC_DATA_ATTR int bootCount = 0;
|
|
esp_sleep_source_t wakeCause; // the reason we booted this time
|
|
|
|
char buffer[40]; // Screen buffer
|
|
|
|
dr_t lorawan_sf; // prefs LORAWAN_SF
|
|
char sf_name[40];
|
|
|
|
unsigned long int ack_req = 0;
|
|
unsigned long int ack_rx = 0;
|
|
|
|
// Same format as CubeCell mappers
|
|
void buildPacket(uint8_t txBuffer[]) {
|
|
uint32_t LatitudeBinary;
|
|
uint32_t LongitudeBinary;
|
|
uint16_t altitudeGps;
|
|
// uint8_t hdopGps;
|
|
uint8_t sats;
|
|
uint16_t speed;
|
|
|
|
LatitudeBinary = ((gps_latitude() + 90) / 180.0) * 16777215;
|
|
LongitudeBinary = ((gps_longitude() + 180) / 360.0) * 16777215;
|
|
altitudeGps = (uint16_t)gps_altitude();
|
|
speed = (uint16_t)gps_speed(); // convert from float
|
|
sats = gps_sats();
|
|
|
|
sprintf(buffer, "Lat: %f, ", gps_latitude());
|
|
Serial.print(buffer);
|
|
sprintf(buffer, "Long: %f, ", gps_longitude());
|
|
Serial.print(buffer);
|
|
sprintf(buffer, "Alt: %f, ", gps_altitude());
|
|
Serial.print(buffer);
|
|
sprintf(buffer, "Sats: %d", sats);
|
|
Serial.println(buffer);
|
|
|
|
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;
|
|
txBuffer[6] = (altitudeGps >> 8) & 0xFF;
|
|
txBuffer[7] = altitudeGps & 0xFF;
|
|
|
|
txBuffer[8] = ((unsigned char *)(&speed))[0];
|
|
|
|
uint16_t batteryVoltage = ((float_t)((float_t)(axp.getBattVoltage()) / 10.0) + .5);
|
|
txBuffer[9] = (uint8_t)((batteryVoltage - 200) & 0xFF);
|
|
|
|
txBuffer[10] = sats & 0xFF;
|
|
}
|
|
|
|
// Send a packet, if one is warranted
|
|
bool trySend() {
|
|
float now_lat = gps_latitude();
|
|
float now_long = gps_longitude();
|
|
unsigned long int now_millis = millis();
|
|
|
|
// Here we try to filter out bogus GPS readings.
|
|
// It's not correct, and there should be a better indication from GPS that the
|
|
// fix is invalid
|
|
if (gps_hdop() <= 0 || gps_hdop() > 50 || now_lat == 0.0 // Not fair to the whole equator
|
|
|| now_lat > 90.0 || now_lat < -90.0 || now_long == 0.0 // Not fair to King George
|
|
|| now_long < -180.0 || now_long > 180.0 || gps_altitude() == 0.0 // Not fair to the ocean
|
|
|| gps_sats() < 4)
|
|
return false; // Rejected as bogus GPS reading.
|
|
|
|
// Don't attempt to send or update until we join Helium
|
|
if (!isJoined)
|
|
return false;
|
|
|
|
// LoRa is not ready for a new packet, maybe still sending the last one.
|
|
if (!LMIC_queryTxReady())
|
|
return false;
|
|
|
|
// Check if there is not a current TX/RX job running
|
|
if (LMIC.opmode & OP_TXRXPEND)
|
|
return false;
|
|
|
|
// distance from last transmitted location
|
|
float dist_moved = gps_distanceBetween(last_send_lat, last_send_lon, now_lat, now_long);
|
|
float deadzone_dist = gps_distanceBetween(deadzone_lat, deadzone_lon, now_lat, now_long);
|
|
in_deadzone = (deadzone_dist <= deadzone_radius_m);
|
|
|
|
/*
|
|
Serial.printf("[Time %lu / %us, Moved %dm in %lus %c]\n", (now_millis - last_send_millis) / 1000, tx_interval_s, (int32_t)dist_moved,
|
|
(now_millis - last_moved_millis) / 1000, in_deadzone ? 'D' : '-');
|
|
*/
|
|
|
|
// Deadzone means we don't send unless asked
|
|
if (in_deadzone && !justSendNow)
|
|
return false;
|
|
|
|
char because = '?';
|
|
if (justSendNow) {
|
|
justSendNow = false;
|
|
Serial.println("** JUST_SEND_NOW");
|
|
because = '>';
|
|
} else if (dist_moved > min_dist_moved) {
|
|
Serial.println("** MOVING");
|
|
last_moved_millis = now_millis;
|
|
because = 'D';
|
|
} else if (now_millis - last_send_millis > tx_interval_s * 1000) {
|
|
Serial.println("** TIME");
|
|
because = 'T';
|
|
} else {
|
|
return false; // Nothing to do, go home early
|
|
}
|
|
|
|
// SEND a Packet!
|
|
// digitalWrite(RED_LED, LOW);
|
|
|
|
// The first distance-moved is crazy, since has no origin.. don't put it on
|
|
// screen.
|
|
if (dist_moved > 1000000)
|
|
dist_moved = 0;
|
|
|
|
snprintf(buffer, sizeof(buffer), "\n%d %c %4lus %4.0fm ", ttn_get_count(), because, (now_millis - last_send_millis) / 1000, dist_moved);
|
|
screen_print(buffer);
|
|
|
|
// prepare the LoRa frame
|
|
buildPacket(txBuffer);
|
|
|
|
// Want an ACK on this one?
|
|
bool confirmed = (LORAWAN_CONFIRMED_EVERY > 0) && (ttn_get_count() % LORAWAN_CONFIRMED_EVERY == 0);
|
|
if (confirmed) {
|
|
Serial.println("ACK requested");
|
|
screen_print("? ");
|
|
digitalWrite(RED_LED, LOW); // Light LED
|
|
ack_req++;
|
|
}
|
|
|
|
// send it!
|
|
|
|
packetQueued = true;
|
|
if (!ttn_send(txBuffer, sizeof(txBuffer), LORAWAN_PORT, confirmed)) {
|
|
Serial.println("Surprise send failure!");
|
|
return false;
|
|
}
|
|
|
|
last_send_millis = now_millis;
|
|
last_send_lat = now_lat;
|
|
last_send_lon = now_long;
|
|
|
|
return true; // We did it!
|
|
}
|
|
|
|
/*
|
|
Mapper namespace
|
|
mapper {
|
|
|
|
}
|
|
*/
|
|
|
|
void mapper_restore_prefs(void) {
|
|
Preferences p;
|
|
if (p.begin("mapper", true)) // Read-only
|
|
{
|
|
min_dist_moved = p.getFloat("min_dist", MIN_DIST);
|
|
rest_wait_s = p.getUInt("rest_wait", REST_WAIT);
|
|
rest_tx_interval_s = p.getUInt("rest_tx", REST_TX_INTERVAL);
|
|
stationary_tx_interval_s = p.getUInt("tx_interval", STATIONARY_TX_INTERVAL);
|
|
if (sizeof(lorawan_sf) != sizeof(unsigned char))
|
|
Serial.println("Error! size mismatch for sf");
|
|
lorawan_sf = p.getUChar("sf", LORAWAN_SF);
|
|
// Close the Preferences
|
|
p.end();
|
|
} else {
|
|
Serial.println("No Mapper prefs -- using defaults.");
|
|
min_dist_moved = MIN_DIST;
|
|
rest_wait_s = REST_WAIT;
|
|
rest_tx_interval_s = REST_TX_INTERVAL;
|
|
stationary_tx_interval_s = STATIONARY_TX_INTERVAL;
|
|
lorawan_sf = LORAWAN_SF;
|
|
}
|
|
|
|
tx_interval_s = stationary_tx_interval_s;
|
|
}
|
|
|
|
void mapper_save_prefs(void) {
|
|
Preferences p;
|
|
|
|
Serial.println("Saving prefs.");
|
|
if (p.begin("mapper", false)) {
|
|
p.putFloat("min_dist", min_dist_moved);
|
|
p.putUInt("rest_wait", rest_wait_s);
|
|
p.putUInt("rest_tx", rest_tx_interval_s);
|
|
p.putUInt("tx_interval", stationary_tx_interval_s);
|
|
p.putUChar("sf", lorawan_sf);
|
|
p.end();
|
|
}
|
|
}
|
|
|
|
void mapper_erase_prefs(void) {
|
|
#if 0
|
|
nvs_flash_erase(); // erase the NVS partition and...
|
|
nvs_flash_init(); // initialize the NVS partition.
|
|
#endif
|
|
Preferences p;
|
|
if (p.begin("mapper", false)) {
|
|
p.clear();
|
|
p.end();
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
void doDeepSleep(uint64_t msecToWake)
|
|
{
|
|
Serial.printf("Entering deep sleep for %llu seconds\n", msecToWake / 1000);
|
|
|
|
// not using wifi yet, but once we are this is needed to shutoff the radio hw
|
|
// esp_wifi_stop();
|
|
|
|
screen_off(); // datasheet says this will draw only 10ua
|
|
LMIC_shutdown(); // cleanly shutdown the radio
|
|
|
|
if (axp192_found) {
|
|
// turn on after initial testing with real hardware
|
|
axp.setPowerOutPut(AXP192_LDO2, AXP202_OFF); // LORA radio
|
|
axp.setPowerOutPut(AXP192_LDO3, AXP202_OFF); // GPS main power
|
|
}
|
|
|
|
// FIXME - use an external 10k pulldown so we can leave the RTC peripherals powered off
|
|
// until then we need the following lines
|
|
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
|
|
|
// Only GPIOs which are have RTC functionality can be used in this bit map: 0,2,4,12-15,25-27,32-39.
|
|
uint64_t gpioMask = (1ULL << MIDDLE_BUTTON_PIN);
|
|
|
|
// FIXME change polarity so we can wake on ANY_HIGH instead - that would allow us to use all three buttons (instead of just the first)
|
|
gpio_pullup_en((gpio_num_t) MIDDLE_BUTTON_PIN);
|
|
|
|
esp_sleep_enable_ext1_wakeup(gpioMask, ESP_EXT1_WAKEUP_ALL_LOW);
|
|
|
|
esp_sleep_enable_timer_wakeup(msecToWake * 1000ULL); // call expects usecs
|
|
esp_deep_sleep_start(); // TBD mA sleep current (battery)
|
|
}
|
|
#endif
|
|
|
|
// LoRa message event callback
|
|
void lora_msg_callback(uint8_t message) {
|
|
static boolean seen_joined = false, seen_joining = false;
|
|
#ifdef DEBUG_LORA_MESSAGES
|
|
{
|
|
snprintf(buffer, sizeof(buffer), "## MSG %d\n", message);
|
|
screen_print(buffer);
|
|
}
|
|
if (EV_JOIN_TXCOMPLETE == message)
|
|
Serial.println("# JOIN_TXCOMPLETE");
|
|
if (EV_TXCOMPLETE == message)
|
|
Serial.println("# TXCOMPLETE");
|
|
if (EV_RXCOMPLETE == message)
|
|
Serial.println("# RXCOMPLETE");
|
|
if (EV_RXSTART == message)
|
|
Serial.println("# RXSTART");
|
|
if (EV_TXCANCELED == message)
|
|
Serial.println("# TXCANCELED");
|
|
if (EV_TXSTART == message)
|
|
Serial.println("# TXSTART");
|
|
if (EV_JOINING == message)
|
|
Serial.println("# JOINING");
|
|
if (EV_JOINED == message)
|
|
Serial.println("# JOINED");
|
|
if (EV_JOIN_FAILED == message)
|
|
Serial.println("# JOIN_FAILED");
|
|
if (EV_REJOIN_FAILED == message)
|
|
Serial.println("# REJOIN_FAILED");
|
|
if (EV_RESET == message)
|
|
Serial.println("# RESET");
|
|
if (EV_LINK_DEAD == message)
|
|
Serial.println("# LINK_DEAD");
|
|
if (EV_ACK == message)
|
|
Serial.println("# ACK");
|
|
if (EV_PENDING == message)
|
|
Serial.println("# PENDING");
|
|
if (EV_QUEUED == message)
|
|
Serial.println("# QUEUED");
|
|
#endif
|
|
|
|
/* This is confusing because JOINED is sometimes spoofed and comes early */
|
|
if (EV_JOINED == message)
|
|
seen_joined = true;
|
|
if (EV_JOINING == message)
|
|
seen_joining = true;
|
|
if (!isJoined && seen_joined && seen_joining) {
|
|
isJoined = true;
|
|
screen_print("Joined Helium!\n");
|
|
ttn_set_sf(lorawan_sf); // Joining seems to leave it at SF10?
|
|
ttn_get_sf_name(sf_name, sizeof(sf_name));
|
|
justSendNow = true;
|
|
}
|
|
|
|
if (EV_TXSTART == message) {
|
|
screen_print("+\v");
|
|
screen_update();
|
|
}
|
|
// We only want to say 'packetSent' for our packets (not packets needed for
|
|
// joining)
|
|
if (EV_TXCOMPLETE == message && packetQueued) {
|
|
// screen_print("sent.\n");
|
|
packetQueued = false;
|
|
if (axp192_found)
|
|
axp.setChgLEDMode(AXP20X_LED_OFF);
|
|
}
|
|
|
|
if (EV_ACK == message) {
|
|
digitalWrite(RED_LED, HIGH);
|
|
ack_rx++;
|
|
Serial.printf("ACK! %lu / %lu\n", ack_rx, ack_req);
|
|
screen_print("! ");
|
|
}
|
|
|
|
if (EV_RXCOMPLETE == message || EV_RESPONSE == message) {
|
|
size_t len = ttn_response_len();
|
|
uint8_t data[len];
|
|
uint8_t port;
|
|
ttn_response(&port, data, len);
|
|
|
|
snprintf(buffer, sizeof(buffer), "\nRx: %d on P%d\n", len, port);
|
|
screen_print(buffer);
|
|
|
|
Serial.printf("Downlink on port: %d = ", port);
|
|
for (int i = 0; i < len; i++) {
|
|
if (data[i] < 16)
|
|
Serial.print('0');
|
|
Serial.print(data[i], HEX);
|
|
}
|
|
Serial.println();
|
|
|
|
/*
|
|
* Downlink format: FPort 1
|
|
* 2 Bytes: Minimum Distance (1 to 65535) meters, or 0 no-change
|
|
* 2 Bytes: Minimum Time (1 to 65535) seconds (18.2 hours) between pings, or
|
|
* 0 no-change, or 0xFFFF to use default 1 Byte: Battery voltage (2.0
|
|
* to 4.5) for auto-shutoff, or 0 no-change
|
|
*/
|
|
if (port == 1 && len == 5) {
|
|
float new_distance = (float)(data[0] << 8 | data[1]);
|
|
if (new_distance > 0.0) {
|
|
min_dist_moved = new_distance;
|
|
snprintf(buffer, sizeof(buffer), "\nNew Dist: %.0fm\n", new_distance);
|
|
screen_print(buffer);
|
|
}
|
|
|
|
unsigned long int new_interval = data[2] << 8 | data[3];
|
|
if (new_interval) {
|
|
if (new_interval == 0xFFFF) {
|
|
stationary_tx_interval_s = STATIONARY_TX_INTERVAL;
|
|
} else {
|
|
stationary_tx_interval_s = new_interval;
|
|
}
|
|
tx_interval_s = stationary_tx_interval_s;
|
|
snprintf(buffer, sizeof(buffer), "\nNew Time: %.0lus\n", new_interval);
|
|
screen_print(buffer);
|
|
}
|
|
|
|
if (data[4]) {
|
|
float new_low_voltage = data[4] / 100.00 + 2.00;
|
|
battery_low_voltage = new_low_voltage;
|
|
snprintf(buffer, sizeof(buffer), "\nNew LowBat: %.2fv\n", new_low_voltage);
|
|
screen_print(buffer);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void scanI2Cdevice(void) {
|
|
byte err, addr;
|
|
int nDevices = 0;
|
|
for (addr = 1; addr < 127; addr++) {
|
|
Wire.beginTransmission(addr);
|
|
err = Wire.endTransmission();
|
|
if (err == 0) {
|
|
#if 0
|
|
Serial.print("I2C device found at address 0x");
|
|
if (addr < 16)
|
|
Serial.print("0");
|
|
Serial.print(addr, HEX);
|
|
Serial.println(" !");
|
|
#endif
|
|
nDevices++;
|
|
|
|
if (addr == SSD1306_ADDRESS) {
|
|
ssd1306_found = true;
|
|
Serial.println("SSD1306 OLED display");
|
|
}
|
|
if (addr == AXP192_SLAVE_ADDRESS) {
|
|
axp192_found = true;
|
|
Serial.println("AXP192 PMU");
|
|
}
|
|
} else if (err == 4) {
|
|
Serial.print("Unknow i2c device at 0x");
|
|
if (addr < 16)
|
|
Serial.print("0");
|
|
Serial.println(addr, HEX);
|
|
}
|
|
}
|
|
if (nDevices == 0)
|
|
Serial.println("No I2C devices found!\n");
|
|
/* else Serial.println("done\n"); */
|
|
}
|
|
|
|
/**
|
|
Init the power manager chip
|
|
|
|
axp192 power
|
|
DCDC1 0.7-3.5V @ 1200mA max -> OLED // If you turn this off you'll lose comms
|
|
to the axp192 because the OLED and the axp192 share the same i2c bus use
|
|
ssd1306 sleep mode instead DCDC2 -> unused DCDC3 0.7-3.5V @ 700mA max -> ESP32
|
|
(keep this on!) LDO1 30mA -> "VCC_RTC" charges GPS backup battery // charges
|
|
the tiny J13 battery by the GPS to power the GPS ram (for a couple of days),
|
|
can not be turned off LDO2 200mA -> "LORA_VCC" LDO3 200mA -> "GPS_VCC"
|
|
*/
|
|
|
|
void axp192Init() {
|
|
if (axp192_found) {
|
|
if (!axp.begin(Wire, AXP192_SLAVE_ADDRESS)) {
|
|
// Serial.println("AXP192 Begin PASS");
|
|
} else {
|
|
Serial.println("axp.begin() FAIL");
|
|
axp192_found = false;
|
|
return;
|
|
}
|
|
|
|
axp.setPowerOutPut(AXP192_LDO2, AXP202_ON); // LORA radio
|
|
axp.setPowerOutPut(AXP192_LDO3, AXP202_ON); // GPS main power
|
|
axp.setLDO3Voltage(3300); // For GPS Power. Can run on 2.7v to 3.6v
|
|
axp.setPowerOutPut(AXP192_DCDC1, AXP202_ON); // OLED power
|
|
axp.setDCDC1Voltage(3300); // for the OLED power
|
|
axp.setPowerOutPut(AXP192_DCDC2, AXP202_OFF); // Unconnected
|
|
axp.setPowerOutPut(AXP192_EXTEN,
|
|
AXP202_OFF); // "EXTEN" pin, normally unused
|
|
|
|
// Flash the Blue LED until our first packet is transmitted
|
|
axp.setChgLEDMode(AXP20X_LED_BLINK_4HZ);
|
|
// axp.setChgLEDMode(AXP20X_LED_OFF);
|
|
|
|
#if 0
|
|
Serial.printf("DCDC1: %s\n", axp.isDCDC1Enable() ? "ENABLE" : "DISABLE");
|
|
Serial.printf("DCDC2: %s\n", axp.isDCDC2Enable() ? "ENABLE" : "DISABLE");
|
|
Serial.printf("DCDC3: %s\n", axp.isDCDC3Enable() ? "ENABLE" : "DISABLE");
|
|
//Serial.printf("LDO1: %s\n", axp.isLDO1Enable() ? "ENABLE" : "DISABLE");
|
|
Serial.printf("LDO2: %s\n", axp.isLDO2Enable() ? "ENABLE" : "DISABLE");
|
|
Serial.printf("LDO3: %s\n", axp.isLDO3Enable() ? "ENABLE" : "DISABLE");
|
|
Serial.printf("Exten: %s\n", axp.isExtenEnable() ? "ENABLE" : "DISABLE");
|
|
#endif
|
|
|
|
pinMode(PMU_IRQ, INPUT_PULLUP);
|
|
attachInterrupt(
|
|
PMU_IRQ, [] { pmu_irq = true; }, FALLING);
|
|
|
|
// Configure REG 36H: PEK press key parameter set. Index values for
|
|
// argument!
|
|
axp.setStartupTime(2); // "Power on time": 512mS
|
|
axp.setlongPressTime(2); // "Long time key press time": 2S
|
|
axp.setShutdownTime(2); // "Power off time" = 8S
|
|
axp.setTimeOutShutdown(1); // "When key press time is longer than power off
|
|
// time, auto power off"
|
|
|
|
// Serial.printf("AC IN: %fv\n", axp.getAcinVoltage());
|
|
// Serial.printf("Vbus: %fv\n", axp.getVbusVoltage());
|
|
Serial.printf("PMIC Temp %0.2f°C\n", axp.getTemp());
|
|
// Serial.printf("TSTemp %f°C\n", axp.getTSTemp());
|
|
// Serial.printf("GPIO0 %fv\n", axp.getGPIO0Voltage());
|
|
// Serial.printf("GPIO1 %fv\n", axp.getGPIO1Voltage());
|
|
// Serial.printf("Batt In: %fmW\n", axp.getBattInpower());
|
|
Serial.printf("Batt: %0.3fv\n", axp.getBattVoltage() / 1000.0);
|
|
Serial.printf("SysIPSOut: %0.3fv\n", axp.getSysIPSOUTVoltage() / 1000.0);
|
|
Serial.printf("isVBUSPlug? %s\n", axp.isVBUSPlug() ? "Yes" : "No");
|
|
Serial.printf("isChargingEnable? %s\n", axp.isChargeingEnable() ? "Yes" : "No");
|
|
Serial.printf("ChargeCurrent: %.2fmA\n", axp.getSettingChargeCurrent());
|
|
Serial.printf("ChargeControlCurrent: %d\n", axp.getChargeControlCur());
|
|
Serial.printf("Charge: %d%%\n", axp.getBattPercentage());
|
|
|
|
Serial.printf("WarningLevel1: %d mV\n", axp.getVWarningLevel1());
|
|
Serial.printf("WarningLevel2: %d mV\n", axp.getVWarningLevel2());
|
|
Serial.printf("PowerDown: %d mV\n", axp.getPowerDownVoltage());
|
|
|
|
Serial.printf("DCDC1Voltage: %d mV\n", axp.getDCDC1Voltage());
|
|
Serial.printf("DCDC2Voltage: %d mV\n", axp.getDCDC2Voltage());
|
|
Serial.printf("DCDC3Voltage: %d mV\n", axp.getDCDC3Voltage());
|
|
Serial.printf("LDO2: %d mV\n", axp.getLDO2Voltage());
|
|
Serial.printf("LDO3: %d mV\n", axp.getLDO3Voltage());
|
|
Serial.printf("LDO4: %d mV\n", axp.getLDO4Voltage());
|
|
|
|
// Enable battery current measurements
|
|
axp.adc1Enable(AXP202_BATT_CUR_ADC1, 1);
|
|
// axp.enableIRQ(AXP202_VBUS_REMOVED_IRQ | AXP202_VBUS_CONNECT_IRQ |
|
|
// AXP202_BATT_REMOVED_IRQ | AXP202_BATT_CONNECT_IRQ, 1);
|
|
axp.enableIRQ(0xFFFFFFFFFF, 1); // Give me ALL the interrupts you have.
|
|
|
|
// @Kenny_PDY discovered that low-battery voltage inhibits detecting the menu button.
|
|
// I don't know why, but might be a persistent interrupt that blocks the button?
|
|
axp.enableIRQ(APX202_APS_LOW_VOL_LEVEL1_IRQ | AXP202_APS_LOW_VOL_LEVEL2_IRQ, 0);
|
|
|
|
axp.clearIRQ();
|
|
} else {
|
|
Serial.println("AXP192 not found!");
|
|
}
|
|
}
|
|
|
|
// Perform power on init that we do on each wake from deep sleep
|
|
void wakeup() {
|
|
bootCount++;
|
|
wakeCause = esp_sleep_get_wakeup_cause();
|
|
/*
|
|
Not using yet because we are using wake on all buttons being low
|
|
|
|
wakeButtons = esp_sleep_get_ext1_wakeup_status(); // If one of these
|
|
buttons is set it was the reason we woke if (wakeCause ==
|
|
ESP_SLEEP_WAKEUP_EXT1 && !wakeButtons) // we must have been using the 'all
|
|
buttons rule for waking' to support busted boards, assume button one was
|
|
pressed wakeButtons = ((uint64_t)1) << buttons.gpios[0];
|
|
*/
|
|
|
|
Serial.printf("BOOT #%d! cause:%d ext1:%08llx\n", bootCount, wakeCause, esp_sleep_get_ext1_wakeup_status());
|
|
}
|
|
|
|
void setup() {
|
|
// Debug
|
|
#ifdef DEBUG_PORT
|
|
DEBUG_PORT.begin(SERIAL_BAUD);
|
|
#endif
|
|
wakeup();
|
|
|
|
Wire.begin(I2C_SDA, I2C_SCL);
|
|
scanI2Cdevice();
|
|
|
|
axp192Init();
|
|
|
|
// GPS sometimes gets wedged with no satellites in view and only a power-cycle
|
|
// saves it. Here we turn off power and the delay in screen setup is enough
|
|
// time to bonk the GPS
|
|
axp.setPowerOutPut(AXP192_LDO3, AXP202_OFF); // GPS power off
|
|
|
|
// Buttons & LED
|
|
pinMode(MIDDLE_BUTTON_PIN, INPUT_PULLUP);
|
|
pinMode(RED_LED, OUTPUT);
|
|
digitalWrite(RED_LED, HIGH); // Off
|
|
|
|
// Hello
|
|
DEBUG_MSG("\n" APP_NAME " " APP_VERSION "\n");
|
|
|
|
mapper_restore_prefs(); // Fetch saved settings
|
|
|
|
// Don't init display if we don't have one or we are waking headless due to a
|
|
// timer event
|
|
if (0 && wakeCause == ESP_SLEEP_WAKEUP_TIMER)
|
|
ssd1306_found = false; // forget we even have the hardware
|
|
|
|
if (ssd1306_found)
|
|
screen_setup();
|
|
|
|
// GPS power on, so it has time to setttle.
|
|
axp.setPowerOutPut(AXP192_LDO3, AXP202_ON);
|
|
|
|
// Show logo on first boot after removing battery
|
|
#ifndef ALWAYS_SHOW_LOGO
|
|
if (bootCount <= 1)
|
|
#endif
|
|
{
|
|
screen_print(APP_NAME " " APP_VERSION, 0, 0); // Above the Logo
|
|
screen_print(APP_NAME " " APP_VERSION "\n"); // Add it to the log too
|
|
|
|
screen_show_logo();
|
|
screen_update();
|
|
delay(LOGO_DELAY);
|
|
}
|
|
|
|
// Helium setup
|
|
if (!ttn_setup()) {
|
|
screen_print("[ERR] Radio module not found!\n");
|
|
|
|
if (REQUIRE_RADIO) {
|
|
screen_off();
|
|
sleep_forever();
|
|
}
|
|
} else {
|
|
ttn_register(lora_msg_callback);
|
|
ttn_join();
|
|
ttn_adr(LORAWAN_ADR);
|
|
}
|
|
|
|
// Might have to add a longer delay here
|
|
gps_setup(); // Init GPS baudrate and messages
|
|
}
|
|
|
|
// Power OFF -- does not return
|
|
void clean_shutdown(void) {
|
|
LMIC_shutdown(); // cleanly shutdown the radio
|
|
mapper_save_prefs();
|
|
ttn_write_prefs();
|
|
if (axp192_found) {
|
|
axp.setChgLEDMode(AXP20X_LED_OFF); // Surprisingly sticky if you don't set it
|
|
|
|
axp.shutdown(); // PMIC power off
|
|
} else {
|
|
while (1)
|
|
; // ?? What to do here
|
|
}
|
|
}
|
|
|
|
void update_activity() {
|
|
float bat_volts = axp.getBattVoltage() / 1000;
|
|
float charge_ma = axp.getBattChargeCurrent();
|
|
// float discharge_ma = axp.getBatChargeCurrent();
|
|
|
|
if (axp192_found && axp.isBatteryConnect() && bat_volts < battery_low_voltage && charge_ma < 99.0) {
|
|
Serial.println("Low Battery OFF");
|
|
screen_print("\nLow Battery OFF\n");
|
|
delay(4999); // Give some time to read the screen
|
|
clean_shutdown();
|
|
}
|
|
|
|
if (millis() - last_moved_millis > rest_wait_s * 1000)
|
|
tx_interval_s = rest_tx_interval_s;
|
|
else
|
|
tx_interval_s = stationary_tx_interval_s;
|
|
}
|
|
|
|
/* I must know what that interrupt was for! */
|
|
const char *find_irq_name(void) {
|
|
const char *irq_name = "MysteryIRQ";
|
|
|
|
if (axp.isAcinOverVoltageIRQ())
|
|
irq_name = "AcinOverVoltage";
|
|
else if (axp.isAcinPlugInIRQ())
|
|
irq_name = "AcinPlugIn";
|
|
else if (axp.isAcinRemoveIRQ())
|
|
irq_name = "AcinRemove";
|
|
else if (axp.isVbusOverVoltageIRQ())
|
|
irq_name = "VbusOverVoltage";
|
|
else if (axp.isVbusPlugInIRQ())
|
|
irq_name = "VbusPlugIn";
|
|
else if (axp.isVbusRemoveIRQ())
|
|
irq_name = "VbusRemove";
|
|
else if (axp.isVbusLowVHOLDIRQ())
|
|
irq_name = "VbusLowVHOLD";
|
|
else if (axp.isBattPlugInIRQ())
|
|
irq_name = "BattPlugIn";
|
|
else if (axp.isBattRemoveIRQ())
|
|
irq_name = "BattRemove";
|
|
else if (axp.isBattEnterActivateIRQ())
|
|
irq_name = "BattEnterActivate";
|
|
else if (axp.isBattExitActivateIRQ())
|
|
irq_name = "BattExitActivate";
|
|
else if (axp.isChargingIRQ())
|
|
irq_name = "Charging";
|
|
else if (axp.isChargingDoneIRQ())
|
|
irq_name = "ChargingDone";
|
|
else if (axp.isBattTempLowIRQ())
|
|
irq_name = "BattTempLow";
|
|
else if (axp.isBattTempHighIRQ())
|
|
irq_name = "BattTempHigh";
|
|
else if (axp.isChipOvertemperatureIRQ())
|
|
irq_name = "ChipOvertemperature";
|
|
else if (axp.isChargingCurrentLessIRQ())
|
|
irq_name = "ChargingCurrentLess";
|
|
else if (axp.isDC2VoltageLessIRQ())
|
|
irq_name = "DC2VoltageLess";
|
|
else if (axp.isDC3VoltageLessIRQ())
|
|
irq_name = "DC3VoltageLess";
|
|
else if (axp.isLDO3VoltageLessIRQ())
|
|
irq_name = "LDO3VoltageLess";
|
|
else if (axp.isPEKShortPressIRQ())
|
|
irq_name = "PEKShortPress";
|
|
else if (axp.isPEKLongtPressIRQ())
|
|
irq_name = "PEKLongtPress";
|
|
else if (axp.isNOEPowerOnIRQ())
|
|
irq_name = "NOEPowerOn";
|
|
else if (axp.isNOEPowerDownIRQ())
|
|
irq_name = "NOEPowerDown";
|
|
else if (axp.isVBUSEffectiveIRQ())
|
|
irq_name = "VBUSEffective";
|
|
else if (axp.isVBUSInvalidIRQ())
|
|
irq_name = "VBUSInvalid";
|
|
else if (axp.isVUBSSessionIRQ())
|
|
irq_name = "VUBSSession";
|
|
else if (axp.isVUBSSessionEndIRQ())
|
|
irq_name = "VUBSSessionEnd";
|
|
else if (axp.isLowVoltageLevel1IRQ())
|
|
irq_name = "LowVoltageLevel1";
|
|
else if (axp.isLowVoltageLevel2IRQ())
|
|
irq_name = "LowVoltageLevel2";
|
|
else if (axp.isTimerTimeoutIRQ())
|
|
irq_name = "TimerTimeout";
|
|
else if (axp.isPEKRisingEdgeIRQ())
|
|
irq_name = "PEKRisingEdge";
|
|
else if (axp.isPEKFallingEdgeIRQ())
|
|
irq_name = "PEKFallingEdge";
|
|
else if (axp.isGPIO3InputEdgeTriggerIRQ())
|
|
irq_name = "GPIO3InputEdgeTrigger";
|
|
else if (axp.isGPIO2InputEdgeTriggerIRQ())
|
|
irq_name = "GPIO2InputEdgeTrigger";
|
|
else if (axp.isGPIO1InputEdgeTriggerIRQ())
|
|
irq_name = "GPIO1InputEdgeTrigger";
|
|
else if (axp.isGPIO0InputEdgeTriggerIRQ())
|
|
irq_name = "GPIO0InputEdgeTrigger";
|
|
|
|
return irq_name;
|
|
}
|
|
|
|
struct menu_entry {
|
|
const char *name;
|
|
void (*func)(void);
|
|
};
|
|
|
|
void menu_send_now(void) {
|
|
justSendNow = true;
|
|
}
|
|
void menu_power_off(void) {
|
|
screen_print("\nPOWER OFF...\n");
|
|
delay(4000); // Give some time to read the screen
|
|
clean_shutdown();
|
|
}
|
|
void menu_flush_prefs(void) {
|
|
screen_print("\nFlushing Prefs!\n");
|
|
ttn_erase_prefs();
|
|
mapper_erase_prefs();
|
|
delay(5000); // Give some time to read the screen
|
|
ESP.restart();
|
|
}
|
|
void menu_distance_plus(void) {
|
|
min_dist_moved += 5;
|
|
}
|
|
void menu_distance_minus(void) {
|
|
min_dist_moved -= 5;
|
|
if (min_dist_moved < 10)
|
|
min_dist_moved = 10;
|
|
}
|
|
void menu_time_plus(void) {
|
|
stationary_tx_interval_s += 10;
|
|
}
|
|
void menu_time_minus(void) {
|
|
stationary_tx_interval_s -= 10;
|
|
if (stationary_tx_interval_s < 10)
|
|
stationary_tx_interval_s = 10;
|
|
}
|
|
void menu_gps_passthrough(void) {
|
|
axp.setChgLEDMode(AXP20X_LED_BLINK_1HZ);
|
|
axp.setPowerOutPut(AXP192_LDO2, AXP202_OFF); // Kill LORA radio
|
|
gps_passthrough();
|
|
// Does not return.
|
|
}
|
|
void menu_experiment(void) {
|
|
static boolean power_toggle = true;
|
|
|
|
Serial.printf("%f mA %f mW\n", axp.getBattChargeCurrent() - axp.getBattDischargeCurrent(), axp.getBattInpower());
|
|
|
|
axp.setPowerOutPut(AXP192_LDO3,
|
|
power_toggle ? AXP202_ON : AXP202_OFF); // GPS main power
|
|
power_toggle = !power_toggle;
|
|
}
|
|
void menu_deadzone_here(void) {
|
|
deadzone_lat = gps_latitude();
|
|
deadzone_lon = gps_longitude();
|
|
}
|
|
|
|
dr_t sf_list[] = {DR_SF7, DR_SF8, DR_SF9, DR_SF10};
|
|
#define SF_ENTRIES (sizeof(sf_list) / sizeof(sf_list[0]))
|
|
uint8_t sf_index = 0;
|
|
|
|
void menu_change_sf(void) {
|
|
sf_index++;
|
|
if (sf_index >= SF_ENTRIES)
|
|
sf_index = 0;
|
|
|
|
lorawan_sf = sf_list[sf_index];
|
|
ttn_set_sf(lorawan_sf);
|
|
ttn_get_sf_name(sf_name, sizeof(sf_name));
|
|
Serial.printf("New SF: %s\n", sf_name);
|
|
}
|
|
|
|
struct menu_entry menu[] = {{"Send Now", menu_send_now}, {"Power Off", menu_power_off}, {"Distance +", menu_distance_plus},
|
|
{"Distance -", menu_distance_minus}, {"Time +", menu_time_plus}, {"Time -", menu_time_minus},
|
|
{"Change SF", menu_change_sf}, {"Flush Prefs", menu_flush_prefs}, {"USB GPS", menu_gps_passthrough},
|
|
{"Deadzone Here", menu_deadzone_here}, {"Danger", menu_experiment}};
|
|
#define MENU_ENTRIES (sizeof(menu) / sizeof(menu[0]))
|
|
|
|
const char *menu_prev;
|
|
const char *menu_cur;
|
|
const char *menu_next;
|
|
boolean in_menu = false;
|
|
boolean is_highlighted = false;
|
|
int menu_entry = 0;
|
|
static uint32_t menu_idle_start = 0; // what tick should we call this press long enough
|
|
|
|
void menu_press(void) {
|
|
if (in_menu)
|
|
menu_entry = (menu_entry + 1) % MENU_ENTRIES;
|
|
else
|
|
in_menu = true;
|
|
|
|
menu_prev = menu[(menu_entry - 1) % MENU_ENTRIES].name;
|
|
menu_cur = menu[menu_entry].name;
|
|
menu_next = menu[(menu_entry + 1) % MENU_ENTRIES].name;
|
|
|
|
menu_idle_start = millis();
|
|
}
|
|
|
|
void menu_selected(void) {
|
|
menu_idle_start = millis();
|
|
menu[menu_entry].func();
|
|
}
|
|
|
|
void loop() {
|
|
gps_loop();
|
|
ttn_loop();
|
|
|
|
if (in_menu && millis() - menu_idle_start > (5 * 1000))
|
|
in_menu = false;
|
|
|
|
screen_loop(tx_interval_s, min_dist_moved, sf_name, gps_sats(), in_menu, menu_prev, menu_cur, menu_next, is_highlighted, in_deadzone);
|
|
|
|
update_activity();
|
|
|
|
/*
|
|
if (packetSent) {
|
|
packetSent = false;
|
|
} */
|
|
|
|
// If any interrupts on PMIC, report the name
|
|
if (axp192_found && pmu_irq) {
|
|
const char *irq_name;
|
|
pmu_irq = false;
|
|
axp.readIRQ();
|
|
irq_name = find_irq_name();
|
|
|
|
if (axp.isPEKShortPressIRQ())
|
|
menu_press();
|
|
else if (axp.isPEKLongtPressIRQ()) // They want to turn OFF
|
|
menu_power_off();
|
|
else {
|
|
snprintf(buffer, sizeof(buffer), "\n* %s ", irq_name);
|
|
screen_print(buffer);
|
|
}
|
|
axp.clearIRQ();
|
|
}
|
|
|
|
static uint32_t pressTime = 0;
|
|
if (!digitalRead(MIDDLE_BUTTON_PIN)) {
|
|
// Pressure is on
|
|
if (!pressTime) { // just started a new press
|
|
pressTime = millis();
|
|
is_highlighted = true;
|
|
}
|
|
} else if (pressTime) {
|
|
// we just did a release
|
|
if (in_menu)
|
|
menu_selected();
|
|
else {
|
|
screen_print("\nSend! ");
|
|
justSendNow = true;
|
|
}
|
|
is_highlighted = false;
|
|
|
|
if (millis() - pressTime > 1000) {
|
|
// Was a long press
|
|
} else {
|
|
// Was a short press
|
|
}
|
|
pressTime = 0; // Released
|
|
}
|
|
|
|
if (trySend()) {
|
|
// Good send
|
|
if (axp192_found)
|
|
axp.setChgLEDMode(AXP20X_LED_LOW_LEVEL);
|
|
} else {
|
|
// Nothing sent.
|
|
// Do NOT delay() here.. the LoRa receiver and join housekeeping also needs to run!
|
|
}
|
|
}
|