Skip to content

Instantly share code, notes, and snippets.

@ma2shita
Last active January 13, 2023 06:56
Show Gist options
  • Save ma2shita/6963154d5694323fbbb6bdd1c8ceb2ba to your computer and use it in GitHub Desktop.
Save ma2shita/6963154d5694323fbbb6bdd1c8ceb2ba to your computer and use it in GitHub Desktop.
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) 2022 SORACOM, INC.
* Released under the MIT license
* https://opensource.org/licenses/mit-license.php
*/
#include <M5Stack.h>
#include <numeric>
#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