Last active
January 13, 2023 06:56
-
-
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/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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