ToF ranger with 3G ext. board and SORACOM UnifiedEndpoint for M5Stack / See: https://soracom.github.io/iot-recipes/poka-yoke-for-coffee-dispenser-by-m5stack/
/* | |
* Copyright (c) 2020 SORACOM, INC. | |
* Released under the MIT license | |
* https://opensource.org/licenses/mit-license.php | |
*/ | |
#include <M5Stack.h> | |
#include <HTTPClient.h> // Why? see https://qiita.com/ma2shita/items/97bf1a0c3158b848019a | |
#define SerialMon Serial | |
#define TEXT_SIZE 2 | |
//https://www.switch-science.com/catalog/5219/ | |
#define ToF_ADDR 0x29 // I2C address of tof | |
#include <VL53L0X.h> // from Lib. manager: https://github.com/pololu/vl53l0x-arduino/blob/master/examples/Continuous/Continuous.ino | |
VL53L0X tof; | |
#include <ArduinoJson.h> | |
/* Dynamic configuration by SORACOM Air metadata: | |
* example: | |
{ | |
"NOTIFICATION_INTERVAL_SEC": 300, | |
"LCD_TURN_ON_AT_BOOT": true, | |
"_SAMPLING_COUNT": 5, | |
"_SAMPLING_INTERVAL_MS": 50, | |
"_CUT_RANGE_LOW_MM": 5, | |
"_CUT_RANGE_HIGH_MM": 700 | |
} | |
*/ | |
long NOTIFICATION_INTERVAL_SEC = 3 * 60; | |
bool LCD_TURN_ON_AT_BOOT = true; | |
int _SAMPLING_COUNT = 5; | |
int _SAMPLING_INTERVAL_MS = 50; | |
int _CUT_RANGE_LOW_MM = 15; | |
int _CUT_RANGE_HIGH_MM = 700; | |
#define SerialAT Serial2 // `Serial2` is 3G Extension board for M5Stack Basic/Gray | |
#define TINY_GSM_MODEM_UBLOX | |
#include <TinyGsmClient.h> | |
TinyGsm modem(SerialAT); | |
TinyGsmClient socket(modem); | |
template<typename T, typename U> void check_connection_status_also_reconnect(T *modem, U *serialMon) { | |
serialMon->println((modem->isGprsConnected()) ? "isGprsConnected(): true" : "isGprsConnected(): false"); | |
long s = millis(); | |
if (!modem->isGprsConnected()) { | |
serialMon->print(F("modem.restart(): ")); | |
modem->restart(); | |
serialMon->println(F("done")); | |
serialMon->print(F("getModemInfo(): ")); | |
serialMon->println(modem->getModemInfo()); | |
serialMon->print(F("getIMEI(): ")); | |
serialMon->println(modem->getIMEI()); | |
serialMon->print(F("waitForNetwork(): ")); | |
while (!modem->waitForNetwork()) serialMon->print(F(".")); | |
serialMon->println(F("Ok")); | |
serialMon->print(F("gprsConnect(soracom.io): ")); | |
modem->gprsConnect("soracom.io", "sora", "sora"); | |
serialMon->println(F("done")); | |
serialMon->print(F("isNetworkConnected(): ")); | |
while (!modem->isNetworkConnected()) serialMon->print(F(".")); | |
serialMon->println(F("Ok")); | |
serialMon->print(F("localIP(): ")); | |
serialMon->println(modem->localIP()); | |
} | |
long e = millis(); | |
serialMon->print(F("Modem bootup elapsed(ms): ")); | |
serialMon->println(e - s); | |
serialMon->print(F("getSignalQuality(): ")); | |
serialMon->println(modem->getSignalQuality()); | |
} | |
#include <ArduinoHttpClient.h> | |
template<typename T, typename U> String get_value_of(const char *tag_name, T socket, U *serialMon) { | |
char path[255]; | |
sprintf_P(path, PSTR("/v1/subscriber.tags.%s"), tag_name); | |
serialMon->print(F("path=")); | |
serialMon->println(path); | |
HttpClient http = HttpClient(socket, "metadata.soracom.io", 80); | |
http.get(path); | |
int rc = http.responseStatusCode(); | |
String rb = http.responseBody(); | |
http.stop(); | |
serialMon->print(F("responseStatusCode(): ")); | |
serialMon->println(rc); | |
serialMon->print(F("responseBody(): ")); | |
serialMon->println(rb); | |
if (rc != 200) rb = ""; | |
return rb; | |
} | |
template<typename T, typename U> void send_to_cloud(const char *upstream_payload, T socket, U *serialMon) { | |
serialMon->println(upstream_payload); | |
HttpClient http = HttpClient(socket, "uni.soracom.io", 80); | |
http.post("/", "application/json", upstream_payload); | |
serialMon->print(F("responseStatusCode(): ")); | |
serialMon->println(http.responseStatusCode()); | |
http.stop(); | |
} | |
#include <vector> | |
std::vector<int> v; | |
#include <algorithm> | |
template <typename T, typename U> float sampling_measurement(T *ranger, int sampling_count, int sampling_interval_ms, long cut_range_low_mm, long cut_range_hight_mm, U *serialMon) { | |
v.clear(); | |
for (int i = 0 ; i < sampling_count ; i++) { | |
v.push_back(ranger->readRangeContinuousMillimeters()); | |
delay(sampling_interval_ms); | |
} | |
serialMon->print(F("All records: ")); | |
for (const auto &x : v) { | |
serialMon->print(x); | |
serialMon->print(F(", ")); | |
} | |
serialMon->println(); | |
for (auto it = v.begin(); it != v.end();) { // instead of #erase_if(), C++11 not impl. | |
if ( !(cut_range_low_mm <= *it && *it <= cut_range_hight_mm) ) { // cut higher and lower for noise cancel | |
it = v.erase(it); | |
} else { | |
++it; | |
} | |
} | |
serialMon->print(F("Actual records: ")); | |
for (const auto &x : v) { | |
serialMon->print(x); | |
serialMon->print(F(", ")); | |
} | |
serialMon->println(); | |
const auto avg = std::accumulate(v.begin(), v.end(), 0.0) / v.size(); | |
serialMon->print(F("Avg(mm): ")); | |
serialMon->println(avg); | |
return avg; | |
} | |
bool lcd_status = LCD_TURN_ON_AT_BOOT; | |
bool using_cloud = true; | |
#include <Ticker.h> | |
Ticker ticker1; | |
volatile long count = 0; | |
void countdown_ticker_for_notification() { | |
count--; | |
if (count < 0) count = 0; // Avoiding minus | |
} | |
void reset_count() { | |
count = NOTIFICATION_INTERVAL_SEC - 1; // Zero origin | |
} | |
void setup() { | |
M5.begin(); | |
SerialMon.begin(115200); | |
SerialAT.begin(115200, SERIAL_8N1, 16, 17); | |
M5.Lcd.fillScreen(TFT_BLACK); | |
M5.Lcd.setTextSize(TEXT_SIZE); | |
M5.Lcd.setCursor(0, 0); | |
SerialMon.println(F("Boot...")); | |
M5.Lcd.println(F("Boot...")); | |
M5.update(); | |
if (M5.BtnA.isPressed()) { | |
SerialMon.println(F("Skip fetching config from Cloud")); | |
M5.Lcd.println(F("Skip fetching config from Cloud")); | |
delay(2000); | |
using_cloud = false; | |
} | |
const size_t capacity = JSON_OBJECT_SIZE(6) + 140; // Code generate by https://arduinojson.org/v6/assistant/ | |
DynamicJsonDocument doc(capacity); | |
if (using_cloud) { | |
SerialMon.println(F("Trying fetch config from Cloud (Wait about 50 seconds...")); | |
M5.Lcd.println(F("Trying fetch config from Cloud (Wait about 50 seconds...")); | |
check_connection_status_also_reconnect(&modem, &SerialMon); | |
String tag_value = get_value_of("config_json", socket, &SerialMon); | |
DeserializationError err = deserializeJson(doc, tag_value); | |
if (err) { | |
SerialMon.print(F("deserializeJson() failed: ")); | |
SerialMon.println(err.c_str()); | |
SerialMon.println(F("(But ignored.)")); | |
} | |
} | |
// Load value from JSON or set default | |
NOTIFICATION_INTERVAL_SEC = doc["NOTIFICATION_INTERVAL_SEC"] | NOTIFICATION_INTERVAL_SEC; | |
LCD_TURN_ON_AT_BOOT = doc["LCD_TURN_ON_AT_BOOT"] | LCD_TURN_ON_AT_BOOT; | |
_SAMPLING_COUNT = doc["_SAMPLING_COUNT"] | _SAMPLING_COUNT; | |
_SAMPLING_INTERVAL_MS = doc["_SAMPLING_INTERVAL_MS"] | _SAMPLING_INTERVAL_MS; | |
_CUT_RANGE_LOW_MM = doc["_CUT_RANGE_LOW_MM"] | _CUT_RANGE_LOW_MM; | |
_CUT_RANGE_HIGH_MM = doc["_CUT_RANGE_HIGH_MM"] | _CUT_RANGE_HIGH_MM; | |
if (LCD_TURN_ON_AT_BOOT) { | |
M5.Lcd.wakeup(); | |
M5.Lcd.setBrightness(255); | |
} else { | |
M5.Lcd.sleep(); | |
M5.Lcd.setBrightness(0); | |
} | |
M5.Lcd.fillScreen(TFT_BLACK); // for clear | |
M5.Lcd.fillRect(0, M5.Lcd.fontHeight() * 0, M5.Lcd.width(), M5.Lcd.fontHeight() * 1, TFT_DARKGREY); // 0means 0row | |
M5.Lcd.setTextColor(TFT_WHITE); | |
M5.Lcd.setTextDatum(TL_DATUM); | |
M5.Lcd.drawString("Config", 0, 0); | |
M5.Lcd.setCursor(0, M5.Lcd.fontHeight() * 1); // 1means 1row | |
M5.Lcd.setTextColor(TFT_WHITE); | |
M5.Lcd.printf("Notification Int.(s): %3u\r\n", NOTIFICATION_INTERVAL_SEC); | |
M5.Lcd.print("Trun ON LCD at boot: "); M5.Lcd.println((LCD_TURN_ON_AT_BOOT) ? "true" : "false"); | |
M5.Lcd.fillRect(0, M5.Lcd.fontHeight() * 6, M5.Lcd.width(), M5.Lcd.fontHeight() * 1, TFT_DARKGREEN); // 6means 6row | |
M5.Lcd.setTextColor(TFT_WHITE); | |
M5.Lcd.setTextDatum(TL_DATUM); | |
M5.Lcd.drawString("Current Status", 0, M5.Lcd.fontHeight() * 6); // 6means 6row | |
Wire.begin(); | |
tof.setAddress(ToF_ADDR); | |
tof.setTimeout(500); | |
tof.init(); | |
tof.startContinuous(); | |
reset_count(); | |
ticker1.attach_ms(1000, countdown_ticker_for_notification); | |
} | |
void loop() { | |
M5.update(); | |
if (M5.BtnC.wasReleased()) { | |
if (lcd_status) { | |
M5.Lcd.sleep(); | |
M5.Lcd.setBrightness(0); | |
} else { | |
M5.Lcd.wakeup(); | |
M5.Lcd.setBrightness(255); | |
} | |
lcd_status = !lcd_status; | |
} | |
float mm = sampling_measurement(&tof, _SAMPLING_COUNT, _SAMPLING_INTERVAL_MS, _CUT_RANGE_LOW_MM, _CUT_RANGE_HIGH_MM, &SerialMon); | |
M5.Lcd.fillRect(0, M5.Lcd.fontHeight() * 7, M5.Lcd.textWidth("Distance avg.: 999mm"), M5.Lcd.fontHeight() * 1, TFT_BLACK); // 7means 7row | |
M5.Lcd.setTextColor(TFT_WHITE); | |
M5.Lcd.setTextDatum(TL_DATUM); | |
M5.Lcd.setCursor(0, M5.Lcd.fontHeight() * 7); // 7means 7row | |
M5.Lcd.printf("Distance avg.: %3.0f mm\r\n", mm); | |
SerialMon.print(F("countdown: ")); SerialMon.println(count); | |
M5.Lcd.fillRect(0, M5.Lcd.fontHeight() * 9, M5.Lcd.textWidth("Notification Left: 9999s"), M5.Lcd.fontHeight() * 1, TFT_BLACK); // 9means 9row | |
M5.Lcd.setTextColor(TFT_WHITE); | |
M5.Lcd.setTextDatum(TL_DATUM); | |
M5.Lcd.setCursor(0, M5.Lcd.fontHeight() * 9); // 9means 9row | |
M5.Lcd.printf("Notification Left: %4u s\r\n", count); | |
if (count == 0 || M5.BtnB.wasReleased()) { | |
M5.Lcd.fillRect(0, M5.Lcd.fontHeight() * 10, M5.Lcd.textWidth("Send to cloud..."), M5.Lcd.fontHeight() * 1, TFT_BLACK); // 10means 10row | |
if (isnan(mm)) { | |
SerialMon.println(F("Too far. Skip.")); | |
M5.Lcd.setTextColor(TFT_MAROON); | |
M5.Lcd.setTextDatum(TL_DATUM); | |
M5.Lcd.drawString("Too far. Skip.", 0, M5.Lcd.fontHeight() * 10); // 10means 10row | |
delay(1000); | |
} else { | |
SerialMon.println(F("Send to cloud...")); | |
M5.Lcd.setTextColor(TFT_CYAN); | |
M5.Lcd.setTextDatum(TL_DATUM); | |
M5.Lcd.drawString("Send to cloud...", 0, M5.Lcd.fontHeight() * 10); // 10means 10row | |
const size_t capacity = JSON_OBJECT_SIZE(1); // Code generate by https://arduinojson.org/v6/assistant/ | |
DynamicJsonDocument doc(capacity); | |
doc["avg_mm"] = mm; | |
char upstream_payload[512]; | |
serializeJson(doc, upstream_payload); | |
if (using_cloud) { | |
check_connection_status_also_reconnect(&modem, &SerialMon); | |
send_to_cloud(upstream_payload, socket, &SerialMon); | |
} | |
} | |
M5.Lcd.fillRect(0, M5.Lcd.fontHeight() * 10, M5.Lcd.textWidth("Send to cloud..."), M5.Lcd.fontHeight() * 1, TFT_BLACK); // 10means 10row | |
reset_count(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment