kopia lustrzana https://github.com/Max-Plastix/tbeam-helium-mapper
1332 wiersze
41 KiB
C++
1332 wiersze
41 KiB
C++
/*
|
|
Helium Mapper build for LilyGo TTGO T-Beam v1.1 boards.
|
|
Copyright (C) 2021-2022 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
|
|
full lineage on Github, including: Fizzy, longfi-arduino, Kyle T. Gabriel, and Xose Pérez
|
|
|
|
GPL makes this all possible -- continue to modify, extend, and share!
|
|
*/
|
|
|
|
/*
|
|
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"
|
|
|
|
#define FPORT_MAPPER 2 // FPort for Uplink messages -- must match Helium Console Decoder script!
|
|
#define FPORT_STATUS 5
|
|
#define FPORT_GPSLOST 6
|
|
|
|
#define STATUS_BOOT 1
|
|
#define STATUS_USB_ON 2
|
|
#define STATUS_USB_OFF 3
|
|
|
|
// Defined in ttn.ino
|
|
void ttn_register(void (*callback)(uint8_t message));
|
|
|
|
bool justSendNow = true; // Send one at boot, regardless of deadzone?
|
|
unsigned long int last_send_ms = 0; // Time of last uplink
|
|
unsigned long int last_moved_ms = 0; // Time of last movement
|
|
unsigned long int last_gpslost_ms = 0; // Time of last gps-lost packet
|
|
double last_send_lat = 0; // Last known location
|
|
double last_send_lon = 0; //
|
|
double dist_moved = 0; // Distance in m from last uplink
|
|
|
|
// Deadzone (no uplink) location and radius
|
|
double deadzone_lat = DEADZONE_LAT;
|
|
double deadzone_lon = DEADZONE_LON;
|
|
double deadzone_radius_m = DEADZONE_RADIUS_M;
|
|
boolean in_deadzone = false;
|
|
|
|
/* Defaults that can be overwritten by downlink messages */
|
|
/* (32-bit int seconds allows for 50 days) */
|
|
unsigned int stationary_tx_interval_s; // prefs STATIONARY_TX_INTERVAL
|
|
unsigned int rest_wait_s; // prefs REST_WAIT
|
|
unsigned int rest_tx_interval_s; // prefs REST_TX_INTERVAL
|
|
|
|
unsigned int tx_interval_s; // Currently-active time interval
|
|
|
|
enum activity_state {
|
|
ACTIVITY_MOVING,
|
|
ACTIVITY_REST,
|
|
ACTIVITY_SLEEP,
|
|
ACTIVITY_GPS_LOST,
|
|
ACTIVITY_WOKE,
|
|
ACTIVITY_INVALID
|
|
};
|
|
enum activity_state active_state = ACTIVITY_INVALID;
|
|
boolean never_rest = NEVER_REST;
|
|
|
|
// Return status from mapper uplink, since we care about the flavor of the failure
|
|
enum mapper_uplink_result { MAPPER_UPLINK_SUCCESS, MAPPER_UPLINK_BADFIX, MAPPER_UPLINK_NOLORA, MAPPER_UPLINK_NOTYET };
|
|
|
|
/* Maybe these moves to prefs eventually? */
|
|
unsigned int sleep_wait_s = SLEEP_WAIT;
|
|
unsigned int sleep_tx_interval_s = SLEEP_TX_INTERVAL;
|
|
|
|
unsigned int gps_lost_wait_s = GPS_LOST_WAIT;
|
|
unsigned int gps_lost_ping_s = GPS_LOST_PING;
|
|
uint32_t last_fix_time = 0;
|
|
|
|
float battery_low_voltage = BATTERY_LOW_VOLTAGE;
|
|
float min_dist_moved = MIN_DIST;
|
|
|
|
AXP20X_Class axp;
|
|
bool pmu_irq = false; // true when PMU IRQ pending
|
|
|
|
bool oled_found = false;
|
|
bool axp192_found = false;
|
|
uint8_t oled_addr = 0; // i2c address of OLED controller
|
|
|
|
bool packetQueued;
|
|
bool isJoined = false;
|
|
|
|
bool screen_stay_on = false;
|
|
bool screen_stay_off = false;
|
|
bool is_screen_on = true;
|
|
int screen_idle_off_s = SCREEN_IDLE_OFF_S;
|
|
uint32_t screen_last_active_ms = 0;
|
|
boolean in_menu = false;
|
|
boolean have_usb_power = true;
|
|
uint8_t usb_power_count = 0;
|
|
|
|
// 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;
|
|
|
|
// Store Lat & Long in six bytes of payload
|
|
void pack_lat_lon(double lat, double lon) {
|
|
uint32_t LatitudeBinary;
|
|
uint32_t LongitudeBinary;
|
|
LatitudeBinary = ((lat + 90) / 180.0) * 16777215;
|
|
LongitudeBinary = ((lon + 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;
|
|
}
|
|
|
|
uint8_t battery_byte(void) {
|
|
uint16_t batteryVoltage = ((float_t)((float_t)(axp.getBattVoltage()) / 10.0) + .5);
|
|
return (uint8_t)((batteryVoltage - 200) & 0xFF);
|
|
}
|
|
|
|
// Prepare a packet for the Mapper
|
|
void build_mapper_packet() {
|
|
double lat;
|
|
double lon;
|
|
uint16_t altitudeGps;
|
|
uint8_t sats;
|
|
uint16_t speed;
|
|
|
|
lat = tGPS.location.lat();
|
|
lon = tGPS.location.lng();
|
|
pack_lat_lon(lat, lon);
|
|
altitudeGps = (uint16_t)tGPS.altitude.meters();
|
|
speed = (uint16_t)tGPS.speed.kmph(); // convert from double
|
|
if (speed > 255)
|
|
speed = 255; // don't wrap around.
|
|
sats = tGPS.satellites.value();
|
|
|
|
sprintf(buffer, "Lat: %f, ", lat);
|
|
Serial.print(buffer);
|
|
sprintf(buffer, "Long: %f, ", lon);
|
|
Serial.print(buffer);
|
|
sprintf(buffer, "Alt: %f, ", tGPS.altitude.meters());
|
|
Serial.print(buffer);
|
|
sprintf(buffer, "Sats: %d", sats);
|
|
Serial.println(buffer);
|
|
|
|
txBuffer[6] = (altitudeGps >> 8) & 0xFF;
|
|
txBuffer[7] = altitudeGps & 0xFF;
|
|
|
|
txBuffer[8] = speed & 0xFF;
|
|
txBuffer[9] = battery_byte();
|
|
|
|
txBuffer[10] = sats & 0xFF;
|
|
}
|
|
|
|
// Helium requires a FCount reset sometime before hitting 0xFFFF
|
|
// 50,000 makes it obvious it was intentional
|
|
#define MAX_FCOUNT 50000
|
|
|
|
boolean send_uplink(uint8_t *txBuffer, uint8_t length, uint8_t fport, boolean confirmed) {
|
|
if (confirmed) {
|
|
Serial.println("ACK requested");
|
|
screen_print("? ");
|
|
digitalWrite(RED_LED, LOW); // Light LED
|
|
ack_req++;
|
|
}
|
|
|
|
// send it!
|
|
packetQueued = true;
|
|
if (!ttn_send(txBuffer, length, fport, confirmed)) {
|
|
Serial.println("Surprise send failure!");
|
|
return false;
|
|
}
|
|
|
|
// Helium requires a re-join / reset of count to avoid 16bit count rollover
|
|
// Hopefully a device reboot every 50k uplinks is no problem.
|
|
if (ttn_get_count() > MAX_FCOUNT) {
|
|
Serial.println("FCount Rollover!");
|
|
|
|
// I don't understand why this doesn't show at all
|
|
screen_print("\n\nRollover Reset!\n");
|
|
screen_update();
|
|
delay(1000); // Give some time to read the screen
|
|
|
|
ttn_erase_prefs();
|
|
ESP.restart();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool status_uplink(uint8_t status, uint8_t value) {
|
|
if (!SEND_STATUS_UPLINKS)
|
|
return false;
|
|
|
|
pack_lat_lon(last_send_lat, last_send_lon);
|
|
txBuffer[6] = battery_byte();
|
|
txBuffer[7] = status;
|
|
txBuffer[8] = value;
|
|
Serial.printf("Tx: STATUS %d %d\n", status, value);
|
|
screen_print("\nTX STATUS ");
|
|
return send_uplink(txBuffer, 9, FPORT_STATUS, 0);
|
|
}
|
|
|
|
bool gpslost_uplink(void) {
|
|
uint16_t minutes_lost;
|
|
|
|
if (!SEND_GPSLOST_UPLINKS)
|
|
return false;
|
|
|
|
minutes_lost = (last_fix_time - millis()) / 1000 / 60;
|
|
pack_lat_lon(last_send_lat, last_send_lon);
|
|
txBuffer[6] = battery_byte();
|
|
txBuffer[7] = tGPS.satellites.value();
|
|
txBuffer[8] = (minutes_lost >> 8) & 0xFF;
|
|
txBuffer[9] = minutes_lost & 0xFF;
|
|
Serial.printf("Tx: GPSLOST %d\n", minutes_lost);
|
|
screen_print("\nTX GPSLOST ");
|
|
return send_uplink(txBuffer, 10, FPORT_GPSLOST, 0);
|
|
}
|
|
|
|
// Send a packet, if one is warranted
|
|
enum mapper_uplink_result mapper_uplink() {
|
|
double now_lat = tGPS.location.lat();
|
|
double now_lon = tGPS.location.lng();
|
|
unsigned long int now = millis();
|
|
|
|
// Here we try to filter out bogus GPS readings.
|
|
if (!(tGPS.location.isValid() && tGPS.time.isValid() && tGPS.satellites.isValid() && tGPS.hdop.isValid() &&
|
|
tGPS.altitude.isValid() && tGPS.speed.isValid()))
|
|
return MAPPER_UPLINK_BADFIX;
|
|
|
|
// Filter out any reports while we have low satellite count. The receiver can old a fix on 3, but it's poor.
|
|
if (tGPS.satellites.value() < 4)
|
|
return MAPPER_UPLINK_BADFIX;
|
|
|
|
// HDOP is only a hint as to accuracy, but we can assume very bad HDOP is not worth mapping.
|
|
// https://en.wikipedia.org/wiki/Dilution_of_precision_(navigation) suggests 5 is a good cutoff.
|
|
if (tGPS.hdop.hdop() > 5.0)
|
|
return MAPPER_UPLINK_BADFIX;
|
|
|
|
// With the exception of a few places, a perfectly zero lat or long probably means we got a bad reading
|
|
if (now_lat == 0.0 || now_lon == 0.0)
|
|
return MAPPER_UPLINK_BADFIX;
|
|
|
|
// Don't attempt to send or update until we join Helium
|
|
if (!isJoined)
|
|
return MAPPER_UPLINK_NOLORA;
|
|
|
|
// LoRa is not ready for a new packet, maybe still sending the last one.
|
|
if (!LMIC_queryTxReady())
|
|
return MAPPER_UPLINK_NOLORA;
|
|
|
|
// Check if there is not a current TX/RX job running
|
|
if (LMIC.opmode & OP_TXRXPEND)
|
|
return MAPPER_UPLINK_NOLORA;
|
|
|
|
// distance from last transmitted location
|
|
double dist_moved = tGPS.distanceBetween(last_send_lat, last_send_lon, now_lat, now_lon);
|
|
double deadzone_dist = tGPS.distanceBetween(deadzone_lat, deadzone_lon, now_lat, now_lon);
|
|
in_deadzone = (deadzone_dist <= deadzone_radius_m);
|
|
|
|
/*
|
|
Serial.printf("[Time %lu / %us, Moved %dm in %lus %c]\n", (now - last_send_ms) / 1000, tx_interval_s,
|
|
(int32_t)dist_moved, (now - last_moved_ms) / 1000, in_deadzone ? 'D' : '-');
|
|
*/
|
|
|
|
// Deadzone means we don't send unless asked
|
|
if (in_deadzone && !justSendNow)
|
|
return MAPPER_UPLINK_NOTYET;
|
|
|
|
char because = '?';
|
|
if (justSendNow) {
|
|
justSendNow = false;
|
|
Serial.println("** JUST_SEND_NOW");
|
|
because = '>';
|
|
} else if (dist_moved > min_dist_moved) {
|
|
Serial.println("** DIST");
|
|
last_moved_ms = now;
|
|
because = 'D';
|
|
} else if (now - last_send_ms > tx_interval_s * 1000) {
|
|
Serial.println("** TIME");
|
|
because = 'T';
|
|
} else {
|
|
return MAPPER_UPLINK_NOTYET; // Nothing to do, go home early
|
|
}
|
|
|
|
// 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 - last_send_ms) / 1000,
|
|
dist_moved);
|
|
screen_print(buffer);
|
|
|
|
// prepare the LoRa frame
|
|
build_mapper_packet();
|
|
|
|
// Want an ACK on this one?
|
|
bool confirmed = (LORAWAN_CONFIRMED_EVERY > 0) && (ttn_get_count() % LORAWAN_CONFIRMED_EVERY == 0);
|
|
|
|
// Send it!
|
|
if (!send_uplink(txBuffer, 11, FPORT_MAPPER, confirmed))
|
|
return MAPPER_UPLINK_NOLORA;
|
|
|
|
last_send_ms = now;
|
|
last_send_lat = now_lat;
|
|
last_send_lon = now_lon;
|
|
|
|
screen_last_active_ms = now;
|
|
return MAPPER_UPLINK_SUCCESS; // We did it!
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
// 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));
|
|
}
|
|
|
|
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 < 0x7F; 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 == 0x3C || addr == 0x78 || addr == 0x7E) {
|
|
oled_addr = addr;
|
|
oled_found = true;
|
|
Serial.printf("OLED at %02X\n", oled_addr);
|
|
}
|
|
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"); */
|
|
}
|
|
|
|
/* The AXP library computes this incorrectly for AXP192.
|
|
It's just a fixed mapping table from the datasheet */
|
|
int axp_charge_to_ma(int set) {
|
|
switch (set) {
|
|
case 0:
|
|
return 100;
|
|
case 1:
|
|
return 190;
|
|
case 2:
|
|
return 280;
|
|
case 3:
|
|
return 360;
|
|
case 4:
|
|
return 450;
|
|
case 5:
|
|
return 550;
|
|
case 6:
|
|
return 630;
|
|
case 7:
|
|
return 700;
|
|
case 8:
|
|
return 780;
|
|
case 9:
|
|
return 880;
|
|
case 10:
|
|
return 960;
|
|
case 11:
|
|
return 1000;
|
|
case 12:
|
|
return 1080;
|
|
case 13:
|
|
return 1160;
|
|
case 14:
|
|
return 1240;
|
|
case 15:
|
|
return 1320;
|
|
default:
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
Initialize the AXP192 power manager chip.
|
|
|
|
DCDC1 0.7-3.5V @ 1200mA max -> OLED
|
|
If you turn the OLED off, it will drag down the I2C lines and block the bus from the AXP192 which shares it.
|
|
Use SSD1306 sleep mode instead
|
|
|
|
DCDC3 0.7-3.5V @ 700mA max -> ESP32 (keep this on!)
|
|
LDO1 30mA -> "VCC_RTC" charges GPS tiny J13 backup battery
|
|
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 200mA "LORA_VCC"
|
|
axp.setPowerOutPut(AXP192_LDO3, AXP202_ON); // GPS power 200mA "GPS_VCC"
|
|
axp.setLDO3Voltage(3300); // Voltage for GPS Power. (Neo-6 can take 2.7v to 3.6v)
|
|
axp.setPowerOutPut(AXP192_DCDC1, AXP202_ON); // OLED power, 1200mA max "VCC_2.5V"
|
|
axp.setDCDC1Voltage(2500); // Voltage or the OLED SSD1306
|
|
axp.setPowerOutPut(AXP192_DCDC2, AXP202_OFF); // Unconnected
|
|
axp.setPowerOutPut(AXP192_EXTEN, AXP202_OFF); // "EXTEN" pin, unused
|
|
axp.setChargeControlCur(AXP1XX_CHARGE_CUR_550MA); // Default 0x1000 = 780mA, more than we can get from USB
|
|
|
|
// 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
|
|
// Fire an interrupt on falling edge. Note that some IRQs repeat/persist.
|
|
pinMode(PMU_IRQ, INPUT);
|
|
gpio_pullup_en((gpio_num_t)PMU_IRQ);
|
|
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"
|
|
axp.setVWarningLevel1(2950); // These warning IRQs do not clear until charged, and inhibit other IRQs!
|
|
axp.setVWarningLevel2(2900); // We effectively disable them by setting them lower than we'd run
|
|
|
|
// 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("Battery: %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");
|
|
have_usb_power = axp.isVBUSPlug();
|
|
Serial.printf("isChargingEnable? %s\n", axp.isChargeingEnable() ? "Yes" : "No");
|
|
// Doesn't work on AXP192 because it has a different charge current curve:
|
|
// Serial.printf("ChargeCurrent: %.2fmA\n", axp.getSettingChargeCurrent());
|
|
Serial.printf("ChargeControlCurrent: %d = %dmA\n", axp.getChargeControlCur(),
|
|
axp_charge_to_ma(axp.getChargeControlCur()));
|
|
Serial.printf("Battery Charge Level: %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.
|
|
// Disable these two IRQs until we figure out why it blocks the PEK button IRQs.
|
|
// Low battery also seems to inhibit the USB present/lost signal we use to wake up.
|
|
axp.enableIRQ(APX202_APS_LOW_VOL_LEVEL1_IRQ, 0);
|
|
axp.enableIRQ(AXP202_APS_LOW_VOL_LEVEL2_IRQ, 0);
|
|
|
|
// The Charging Current available is less than requested for battery charging.
|
|
// Another Persistent IRQ. Clear it after showing it once?
|
|
// TODO: Show it every X minutes? Adjust charge current request?
|
|
axp.enableIRQ(AXP202_CHARGE_LOW_CUR_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();
|
|
|
|
Serial.printf("BOOT #%d! cause:%d ext1:%08llx\n", bootCount, wakeCause, esp_sleep_get_ext1_wakeup_status());
|
|
}
|
|
|
|
#include <BluetoothSerial.h>
|
|
#include <WiFi.h>
|
|
#include <esp_bt.h>
|
|
|
|
void setup() {
|
|
// Debug
|
|
#ifdef DEBUG_PORT
|
|
DEBUG_PORT.begin(SERIAL_BAUD);
|
|
#endif
|
|
wakeup();
|
|
|
|
// Make sure WiFi and BT are off
|
|
// WiFi.disconnect(true);
|
|
WiFi.mode(WIFI_MODE_NULL);
|
|
btStop();
|
|
|
|
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);
|
|
gpio_pullup_en((gpio_num_t)MIDDLE_BUTTON_PIN);
|
|
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)
|
|
oled_found = false; // forget we even have the hardware
|
|
|
|
// This creates the display object, so if we don't call it.. all screen ops are do-nothing.
|
|
if (oled_found)
|
|
screen_setup(oled_addr);
|
|
is_screen_on = true;
|
|
|
|
// GPS power on, so it has time to setttle.
|
|
axp.setPowerOutPut(AXP192_LDO3, AXP202_ON);
|
|
|
|
// Show logo on first boot (as opposed to wake)
|
|
if (bootCount <= 1) {
|
|
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");
|
|
sleep_forever();
|
|
}
|
|
|
|
ttn_register(lora_msg_callback);
|
|
ttn_join();
|
|
ttn_adr(LORAWAN_ADR);
|
|
|
|
// Might have to add a longer delay here for GPS boot-up. Takes longer to sync if we talk to it too early.
|
|
delay(100);
|
|
gps_setup(true); // Init GPS baudrate and messages
|
|
|
|
// This is bad.. we can't find the AXP192 PMIC, so no menu key detect:
|
|
if (!axp192_found)
|
|
screen_print("** Missing AXP192! **\n");
|
|
|
|
Serial.printf("Deadzone: %f.0m @ %f, %f\n", deadzone_radius_m, deadzone_lat, deadzone_lon);
|
|
}
|
|
|
|
// Should be around 0.5mA ESP32 consumption, plus OLED controller and PMIC overhead.
|
|
void low_power_sleep(uint32_t seconds) {
|
|
boolean was_screen_on = is_screen_on;
|
|
|
|
Serial.printf("Sleep %d..\n", seconds);
|
|
Serial.flush();
|
|
|
|
screen_off();
|
|
|
|
digitalWrite(RED_LED, HIGH); // LED Off
|
|
|
|
if (axp192_found) {
|
|
axp.setPowerOutPut(AXP192_LDO3, AXP202_OFF); // GPS power
|
|
axp.setChgLEDMode(AXP20X_LED_OFF); // Blue LED off
|
|
|
|
// Turning off DCDC1 consumes MORE power, for reasons unknown
|
|
// axp.setPowerOutPut(AXP192_DCDC1, AXP202_OFF); // OLED power off
|
|
// pinMode(I2C_SCL, OUTPUT);
|
|
// digitalWrite(I2C_SCL, HIGH); // Must enable pull-up on SCL to keep AXP accessible
|
|
}
|
|
|
|
// Wake on either button press
|
|
gpio_wakeup_enable((gpio_num_t)MIDDLE_BUTTON_PIN, GPIO_INTR_LOW_LEVEL);
|
|
gpio_wakeup_enable((gpio_num_t)PMU_IRQ, GPIO_INTR_LOW_LEVEL);
|
|
esp_sleep_enable_gpio_wakeup();
|
|
|
|
esp_sleep_enable_timer_wakeup(seconds * 1000ULL * 1000ULL); // call expects usecs
|
|
// Some GPIOs need this to stay on?
|
|
// esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
|
|
|
esp_light_sleep_start();
|
|
// If we woke by keypress (7) then turn on the screen
|
|
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO) {
|
|
// Try not to puke, but we pretend we moved if they hit a key, to exit SLEEP and restart timers
|
|
last_moved_ms = screen_last_active_ms = millis();
|
|
was_screen_on = true; // Lies
|
|
Serial.println("(GPIO)");
|
|
}
|
|
Serial.println("..woke");
|
|
|
|
if (was_screen_on)
|
|
screen_on();
|
|
|
|
if (axp192_found) {
|
|
axp.setPowerOutPut(AXP192_LDO3, AXP202_ON); // GPS power
|
|
// axp.setPowerOutPut(AXP192_DCDC1, AXP202_ON); // OLED power
|
|
// if (oled_found)
|
|
// screen_setup();
|
|
}
|
|
|
|
delay(100); // GPS doesn't respond right away.. not ready for baud-rate test.
|
|
gps_setup(false); // Resync with GPS
|
|
}
|
|
|
|
// 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. burn power?
|
|
}
|
|
}
|
|
|
|
uint32_t woke_time_ms = 0;
|
|
uint32_t woke_fix_count = 0;
|
|
|
|
/* Determine the current activity state */
|
|
void update_activity() {
|
|
static enum activity_state last_active_state = ACTIVITY_INVALID;
|
|
|
|
if (active_state != last_active_state) {
|
|
switch (active_state) {
|
|
case ACTIVITY_MOVING:
|
|
Serial.println("//MOVING//");
|
|
break;
|
|
case ACTIVITY_REST:
|
|
Serial.println("//REST//");
|
|
break;
|
|
case ACTIVITY_SLEEP:
|
|
Serial.println("//SLEEP//");
|
|
break;
|
|
case ACTIVITY_GPS_LOST:
|
|
Serial.println("//GPS_LOST//");
|
|
break;
|
|
case ACTIVITY_WOKE:
|
|
Serial.println("//WOKE//");
|
|
break;
|
|
default:
|
|
Serial.println("//WTF?//");
|
|
break;
|
|
}
|
|
last_active_state = active_state;
|
|
}
|
|
|
|
uint32_t now = millis();
|
|
float bat_volts = axp.getBattVoltage() / 1000;
|
|
float charge_ma = axp.getBattChargeCurrent();
|
|
|
|
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();
|
|
}
|
|
|
|
// Here we just woke from a GPS-off long sleep.
|
|
// When we have a fresh GPS fix, and the fix qualifies for mapper report, we can resume
|
|
// either mapping or going back to sleep. Until then, we loop in Wake looking for a good GPS signal.
|
|
// Note that we have to be sensitive to "good fix, but not interesting" and go right back to sleep.
|
|
// We're only staying awake until we got a good GPS fix or gave up, NOT until we send a mapper report.
|
|
if (active_state == ACTIVITY_WOKE) {
|
|
if (tGPS.sentencesWithFix() != woke_fix_count && mapper_uplink() != MAPPER_UPLINK_BADFIX)
|
|
active_state = ACTIVITY_REST;
|
|
else if (now - woke_time_ms > gps_lost_wait_s * 1000)
|
|
active_state = ACTIVITY_GPS_LOST;
|
|
return; // else stay in WOKE until we make a good report
|
|
}
|
|
|
|
if (active_state == ACTIVITY_SLEEP && !in_menu) {
|
|
low_power_sleep(tx_interval_s);
|
|
active_state = ACTIVITY_WOKE;
|
|
woke_time_ms = millis();
|
|
woke_fix_count = tGPS.sentencesWithFix();
|
|
return;
|
|
}
|
|
|
|
// In order of precedence:
|
|
if (never_rest) {
|
|
active_state = ACTIVITY_MOVING;
|
|
} else if (now - last_moved_ms > sleep_wait_s * 1000) {
|
|
active_state = ACTIVITY_SLEEP;
|
|
} else if (now - last_fix_time > gps_lost_wait_s * 1000) {
|
|
active_state = ACTIVITY_GPS_LOST;
|
|
} else if (now - last_moved_ms > rest_wait_s * 1000) {
|
|
active_state = ACTIVITY_REST;
|
|
} else {
|
|
active_state = ACTIVITY_MOVING;
|
|
}
|
|
|
|
{
|
|
boolean had_usb_power = have_usb_power;
|
|
have_usb_power = (axp192_found && axp.isVBUSPlug());
|
|
|
|
if (have_usb_power && !had_usb_power) {
|
|
usb_power_count++;
|
|
status_uplink(STATUS_USB_ON, usb_power_count);
|
|
}
|
|
if (!have_usb_power && had_usb_power) {
|
|
status_uplink(STATUS_USB_OFF, usb_power_count);
|
|
}
|
|
}
|
|
|
|
// If we have USB power, keep GPS on all the time; don't sleep
|
|
if (have_usb_power) {
|
|
if (active_state == ACTIVITY_SLEEP)
|
|
active_state = ACTIVITY_REST;
|
|
}
|
|
|
|
switch (active_state) {
|
|
case ACTIVITY_MOVING:
|
|
tx_interval_s = stationary_tx_interval_s;
|
|
break;
|
|
case ACTIVITY_REST:
|
|
tx_interval_s = rest_tx_interval_s;
|
|
break;
|
|
case ACTIVITY_GPS_LOST:
|
|
tx_interval_s = gps_lost_ping_s;
|
|
break;
|
|
case ACTIVITY_SLEEP:
|
|
tx_interval_s = sleep_tx_interval_s;
|
|
break;
|
|
default:
|
|
// ???
|
|
tx_interval_s = stationary_tx_interval_s;
|
|
break;
|
|
}
|
|
|
|
// Has the screen been on for longer than idle time?
|
|
if (now - screen_last_active_ms > screen_idle_off_s * 1000) {
|
|
if (is_screen_on && !screen_stay_on) {
|
|
is_screen_on = false;
|
|
screen_off();
|
|
}
|
|
} else { // Else we had some recent activity. Turn on?
|
|
if (!is_screen_on && !screen_stay_off) {
|
|
is_screen_on = true;
|
|
screen_on();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* 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 = "USB Connected"; // "VbusPlugIn";
|
|
else if (axp.isVbusRemoveIRQ())
|
|
irq_name = "USB Removed"; // "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) {
|
|
#if 0
|
|
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;
|
|
#endif
|
|
|
|
Serial.println("sleep 15");
|
|
|
|
#if 0
|
|
screen_end();
|
|
|
|
Serial.println("powering off..");
|
|
|
|
//pinMode(I2C_SDA, OUTPUT);
|
|
//digitalWrite(I2C_SDA, HIGH);
|
|
axp.setPowerOutPut(AXP192_DCDC1, AXP202_OFF); // OLED power, 1200mA max
|
|
pinMode(I2C_SCL, OUTPUT);
|
|
digitalWrite(I2C_SCL, HIGH);
|
|
|
|
delay(5000);
|
|
Serial.println("back on");
|
|
axp.setPowerOutPut(AXP192_DCDC1, AXP202_ON); // OLED power, 1200mA max
|
|
screen_setup();
|
|
#endif
|
|
low_power_sleep(999);
|
|
Serial.println("done.");
|
|
}
|
|
|
|
void menu_deadzone_here(void) {
|
|
if (tGPS.location.isValid()) {
|
|
deadzone_lat = tGPS.location.lat();
|
|
deadzone_lon = tGPS.location.lng();
|
|
deadzone_radius_m = DEADZONE_RADIUS_M;
|
|
}
|
|
}
|
|
void menu_no_deadzone(void) {
|
|
deadzone_radius_m = 0.0;
|
|
}
|
|
|
|
void menu_stay_on(void) {
|
|
screen_stay_on = !screen_stay_on;
|
|
}
|
|
|
|
void menu_gps_reset(void) {
|
|
gps_full_reset();
|
|
}
|
|
|
|
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}, {"Full Reset", menu_flush_prefs}, {"USB GPS", menu_gps_passthrough},
|
|
{"Deadzone Here", menu_deadzone_here}, {"No Deadzone", menu_no_deadzone}, {"Stay On", menu_stay_on},
|
|
{"GPS Reset", menu_gps_reset}, {"Experiment", menu_experiment}};
|
|
#define MENU_ENTRIES (sizeof(menu) / sizeof(menu[0]))
|
|
|
|
const char *menu_prev;
|
|
const char *menu_cur;
|
|
const char *menu_next;
|
|
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 update_screen(void) {
|
|
screen_header(tx_interval_s, min_dist_moved, sf_name, in_deadzone, screen_stay_on, never_rest);
|
|
screen_body(in_menu, menu_prev, menu_cur, menu_next, is_highlighted);
|
|
}
|
|
|
|
void loop() {
|
|
static uint32_t last_fix_count = 0;
|
|
static boolean booted = true;
|
|
uint32_t now_fix_count;
|
|
uint32_t now = millis();
|
|
|
|
gps_loop(0 /* active_state == ACTIVITY_WOKE */); // Update GPS
|
|
now_fix_count = tGPS.sentencesWithFix(); // Did we get a new fix?
|
|
if (now_fix_count != last_fix_count) {
|
|
last_fix_count = now_fix_count;
|
|
last_fix_time = now; // Note the time of most recent fix
|
|
}
|
|
|
|
ttn_loop();
|
|
|
|
// menu timeout
|
|
if (in_menu && now - menu_idle_start > (MENU_TIMEOUT_S)*1000)
|
|
in_menu = false;
|
|
|
|
update_screen();
|
|
|
|
// If any interrupts on PMIC, report the name
|
|
// PEK button handler
|
|
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()) // want to turn OFF
|
|
menu_power_off();
|
|
else {
|
|
snprintf(buffer, sizeof(buffer), "\n* %s ", irq_name);
|
|
screen_print(buffer);
|
|
}
|
|
axp.clearIRQ();
|
|
screen_last_active_ms = now;
|
|
}
|
|
|
|
// Middle Button handler
|
|
static uint32_t pressTime = 0;
|
|
if (!digitalRead(MIDDLE_BUTTON_PIN)) {
|
|
// Pressure is on
|
|
if (!pressTime) { // just started a new press
|
|
pressTime = now;
|
|
screen_last_active_ms = now;
|
|
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 (now - pressTime > 1000) {
|
|
// Was a long press
|
|
} else {
|
|
// Was a short press
|
|
}
|
|
pressTime = 0; // Released
|
|
screen_last_active_ms = now;
|
|
}
|
|
|
|
update_activity();
|
|
|
|
if (booted) {
|
|
// status_uplink(STATUS_BOOT, 0);
|
|
booted = 0;
|
|
}
|
|
|
|
if (active_state == ACTIVITY_GPS_LOST) {
|
|
now = millis();
|
|
if ((last_gpslost_ms == 0) || // first time losing GPS?
|
|
(now - last_gpslost_ms > GPS_LOST_PING * 1000)) {
|
|
gpslost_uplink();
|
|
last_gpslost_ms = now;
|
|
}
|
|
} else {
|
|
if (active_state != ACTIVITY_SLEEP) // not sure about this
|
|
last_gpslost_ms = 0; // Reset if we regained GPS
|
|
}
|
|
|
|
if (mapper_uplink() == MAPPER_UPLINK_SUCCESS) {
|
|
// Good send, light Blue LED
|
|
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!
|
|
}
|
|
}
|