Skip to main content

Develop Edge Impulse application to cloud via Helium

Upgradable to Industrial Sensors

With the SenseCAP S2110 controller and S2100 data logger, you can easily turn the Grove into a LoRaWAN® sensor. Seeed not only helps you with prototyping but also offers you the possibility to expand your project with the SenseCAP series of robust industrial sensors.

The IP66 housing, Bluetooth configuration, compatibility with the global LoRaWAN® network, built-in 19 Ah battery, and powerful support from APP make the SenseCAP S210x the best choice for industrial applications. The series includes sensors for soil moisture, air temperature and humidity, light intensity, CO2, EC, and an 8-in-1 weather station. Try the latest SenseCAP S210x for your next successful industrial project.

SenseCAP Industrial Sensor
S2100
Data Logger
S2101
Air Temp & Humidity
S2102
Light
S2103
Air Temp & Humidity & CO2
S2104
Soil Moisture & Temp
S2105
Soil Moisture & Temp & EC
S2110
LoRaWAN® Controller
S2120
8-in-1 Weather Station

Tools We Use

note

Before starting this section, make sure you have known Wio Terminal Production For more details, please read

This article show a solution for guys wanna use edgeimpulse to generated models and connect with cloud. In our demo , we will use google sheet. It's the directly and

Helium Configuration

Step 1. Create a Integrations with the Google Form support

This step is similar to the steps in the article Integrate into Google Sheets via Helium

The things we need to do is name the integration and simply save the configuration.

Connecting to Google Form

  • Create

  • Connect with Google Sheets

  • Link with Google Form ID

Step 2. Create a Fuction with the Google Form api and Decoder features

Make sure that Google Form is connected with the Fuction ,filled with the ID we got from the above steps.

We need to create a Fuction with the Decoder Support for our data flow transfer , like this.

function Decoder(bytes, port) {

var decoded = {};

function transformers(bytes) {
if (bytes[0] == 255 || bytes[0] == 0) {
value = bytes[2] * 256 + bytes[3];
}
return value;
}

if (port == 8) {
decoded.class = transformers(bytes.slice(0, 4));
}

var decodedPayload = {
"class": decoded.class
};

// END TODO

return Serialize(decodedPayload)
}

var field_mapping = {
"class": "entry.39410305"
};

function Serialize(payload) {
var str = [];
for (var key in payload) {
if (payload.hasOwnProperty(key)) {
var name = encodeURIComponent(field_mapping[key]);
var value = encodeURIComponent(payload[key]);
str.push(name + "=" + value);
}
}
return str.join("&");
}
// DO NOT REMOVE: Google Form Function\

Step 3 . Configure the Flows

Ensure connection

Edge Impulse Configuration

Ardiuno (Wio Terminal) Configuration

note

Because sensors and environments are different, direct burning of models trained on different carrier boards is not always ideal. Reliable models need to be trained by users themselves, so only test code is provided for quick experience.

Have an experience

After we generated the library from Edge Impulse. We need to modify the code for data sending via Lora on Wio Terminal . If you just wanna have an experience, simply copy the code below and flash is on your Wio Terminal via Arduino IDE.

Flash the test code below.

#include <AIot_Example_inferencing.h>
#include"LIS3DHTR.h"
#include"TFT_eSPI.h"
LIS3DHTR<TwoWire> lis;
TFT_eSPI tft;
#include <SoftwareSerial.h>
#include <Arduino.h>
#include <SensirionI2CSht4x.h>
#include <Wire.h>

SoftwareSerial mySerial(A0, A1); // RX, TX

SensirionI2CSht4x sht4x;

static char recv_buf[512];
static bool is_exist = false;
static bool is_join = false;

static int at_send_check_response(char *p_ack, int timeout_ms, char *p_cmd, ...)
{
int ch;
int num = 0;
int index = 0;
int startMillis = 0;
va_list args;
memset(recv_buf, 0, sizeof(recv_buf));
va_start(args, p_cmd);
mySerial.printf(p_cmd, args);
Serial.printf(p_cmd, args);
va_end(args);
delay(200);
startMillis = millis();

if (p_ack == NULL)
{
return 0;
}

do
{
while (mySerial.available() > 0)
{
ch = mySerial.read();
recv_buf[index++] = ch;
Serial.print((char)ch);
delay(2);
}

if (strstr(recv_buf, p_ack) != NULL)
{
return 1;
}

} while (millis() - startMillis < timeout_ms);
return 0;
}

static void recv_prase(char *p_msg)
{
if (p_msg == NULL)
{
return;
}
char *p_start = NULL;
int data = 0;
int rssi = 0;
int snr = 0;

p_start = strstr(p_msg, "RX");
if (p_start && (1 == sscanf(p_start, "RX: \"%d\"\r\n", &data)))
{
Serial.println(data);
}

p_start = strstr(p_msg, "RSSI");
if (p_start && (1 == sscanf(p_start, "RSSI %d,", &rssi)))
{
Serial.println(rssi);
}

p_start = strstr(p_msg, "SNR");
if (p_start && (1 == sscanf(p_start, "SNR %d", &snr)))
{
Serial.println(snr);
}
}
////// Send message block end


/* Constant defines -------------------------------------------------------- */
#define CONVERT_G_TO_MS2 9.80665f
#define MAX_ACCEPTED_RANGE 2.0f // starting 03/2022, models are generated setting range to +-2, but this example use Arudino library which set range to +-4g. If you are using an older model, ignore this value and use 4.0f instead

/* Private variables ------------------------------------------------------- */
static bool debug_nn = false; // Set this to true to see e.g. features generated from the raw signal

/**
* @brief Arduino setup function
*/
void setup()
{
// put your setup code here, to run once:
Serial.begin(115200);
Serial.println("Edge Impulse Inferencing Demo");

tft.begin();
tft.setRotation(3);
tft.fillScreen(TFT_WHITE);

lis.begin(Wire1);

if (!lis.available()) {
Serial.println("Failed to initialize IMU!");
while (1);
}
else {
ei_printf("IMU initialized\r\n");
}
lis.setOutputDataRate(LIS3DHTR_DATARATE_100HZ); // Setting output data rage to 25Hz, can be set up tp 5kHz
lis.setFullScaleRange(LIS3DHTR_RANGE_16G); // Setting scale range to 2g, select from 2,4,8,16g


if (EI_CLASSIFIER_RAW_SAMPLES_PER_FRAME != 3) {
ei_printf("ERR: EI_CLASSIFIER_RAW_SAMPLES_PER_FRAME should be equal to 3 (the 3 sensor axes)\n");
return;
}

mySerial.begin(9600);

Wire.begin();

uint16_t error;
char errorMessage[256];

sht4x.begin(Wire);

uint32_t serialNumber;
error = sht4x.serialNumber(serialNumber);
delay(5000);
if (error) {
Serial.print("Error trying to execute serialNumber(): ");
errorToString(error, errorMessage, 256);
Serial.println(errorMessage);
} else {
Serial.print("Serial Number: ");
Serial.println(serialNumber);
}

Serial.print("E5 LORAWAN TEST\r\n");

if (at_send_check_response("+AT: OK", 100, "AT\r\n"))
{
is_exist = true;
at_send_check_response("+ID: DevEui", 1000, "AT+ID=DevEui,\"608XXXXXXXXEE7\"\r\n");
at_send_check_response("+ID: AppEui", 1000, "AT+ID=AppEui,\"608XXXXXXXX85D\"\r\n");
at_send_check_response("+MODE: LWOTAA", 1000, "AT+MODE=LWOTAA\r\n");
at_send_check_response("+DR: EU868", 1000, "AT+DR=EU868\r\n");
at_send_check_response("+CH: NUM", 1000, "AT+CH=NUM,0-2\r\n");
at_send_check_response("+KEY: APPKEY", 1000, "AT+KEY=APPKEY,\"E1EF1AC8XXXXXXXXXXXXXXXX05C5\"\r\n");
at_send_check_response("+CLASS: A", 1000, "AT+CLASS=A\r\n");
at_send_check_response("+PORT: 8", 1000, "AT+PORT=8\r\n");
delay(200);
is_join = true;
}
else
{
is_exist = false;
Serial.print("No E5 module found.\r\n");
}
}

/**
* @brief Return the sign of the number
*
* @param number
* @return int 1 if positive (or 0) -1 if negative
*/
float ei_get_sign(float number) {
return (number >= 0.0) ? 1.0 : -1.0;
}

/**
* @brief Get data and run inferencing
*
* @param[in] debug Get debug info if true
*/
void loop()
{
ei_printf("\nStarting inferencing in 2 seconds...\n");

delay(2000);

ei_printf("Sampling...\n");

// Allocate a buffer here for the values we'll read from the IMU
float buffer[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE] = { 0 };

for (size_t ix = 0; ix < EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE; ix += 3) {
// Determine the next tick (and then sleep later)
uint64_t next_tick = micros() + (EI_CLASSIFIER_INTERVAL_MS * 1000);
lis.getAcceleration(&buffer[ix], &buffer[ix + 1], &buffer[ix + 2]);

for (int i = 0; i < 3; i++) {
if (fabs(buffer[ix + i]) > MAX_ACCEPTED_RANGE) {
buffer[ix + i] = ei_get_sign(buffer[ix + i]) * MAX_ACCEPTED_RANGE;
}
}

buffer[ix + 0] *= CONVERT_G_TO_MS2;
buffer[ix + 1] *= CONVERT_G_TO_MS2;
buffer[ix + 2] *= CONVERT_G_TO_MS2;

delayMicroseconds(next_tick - micros());
}

// Turn the raw buffer in a signal which we can the classify
signal_t signal;
int err = numpy::signal_from_buffer(buffer, EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE, &signal);
if (err != 0) {
ei_printf("Failed to create signal from buffer (%d)\n", err);
return;
}

// Run the classifier
ei_impulse_result_t result = { 0 };

err = run_classifier(&signal, &result, debug_nn);
if (err != EI_IMPULSE_OK) {
ei_printf("ERR: Failed to run classifier (%d)\n", err);
return;
}

// print the predictions
ei_printf("Predictions ");
ei_printf("(DSP: %d ms., Classification: %d ms., Anomaly: %d ms.)",
result.timing.dsp, result.timing.classification, result.timing.anomaly);
ei_printf(": \n");
for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
ei_printf(" %s: %.5f\n", result.classification[ix].label, result.classification[ix].value);
}
#if EI_CLASSIFIER_HAS_ANOMALY == 1
ei_printf(" anomaly score: %.3f\n", result.anomaly);
#endif

int classification_flag = 0;

if (result.classification[1].value > 0.7) {
tft.fillScreen(TFT_PURPLE);
tft.setFreeFont(&FreeSansBoldOblique12pt7b);
tft.drawString("Wave", 20, 80);
delay(1000);
tft.fillScreen(TFT_WHITE);
classification_flag = 1;
}

if (result.classification[2].value > 0.7) {
tft.fillScreen(TFT_RED);
tft.setFreeFont(&FreeSansBoldOblique12pt7b);
tft.drawString("Circle", 20, 80);
delay(1000);
tft.fillScreen(TFT_WHITE);
classification_flag = 2;
}


if (is_exist){
int ret = 0;
if (is_join){
ret = at_send_check_response("+JOIN: Network joined", 12000, "AT+JOIN\r\n");
if (ret){
is_join = false;
}
else{
Serial.println("");
Serial.print("JOIN failed!\r\n\r\n");
delay(5000);
}
}
else{
char cmd[128];
sprintf(cmd, "AT+CMSGHEX=\"%08X %08X\"\r\n", classification_flag);
ret = at_send_check_response("Done", 10000, cmd);
if (ret){
Serial.print("classification_flag:");
Serial.print(classification_flag);
Serial.print("\t");
recv_prase(recv_buf);
}
else{
Serial.print("Send failed!\r\n\r\n");
}
delay(5000);
}
}
else
{
delay(500);
}

}

#if !defined(EI_CLASSIFIER_SENSOR) || EI_CLASSIFIER_SENSOR != EI_CLASSIFIER_SENSOR_ACCELEROMETER
#error "Invalid model for current sensor"
#endif

DIY for more features

note

For more details, please see the documentation below.

Something we might pay more attention to :

  • Store classification results:

    We can set a threshold to change our flags when a certain condition is present, and different categories are given different labels.

    We can comment tft fuction for speed.

    int classification_flag = 0;
    if (result.classification[1].value > 0.7) {
    tft.fillScreen(TFT_PURPLE);
    tft.setFreeFont(&FreeSansBoldOblique12pt7b);
    tft.drawString("Wave", 20, 80);
    delay(1000);
    tft.fillScreen(TFT_WHITE);
    classification_flag = 1;
    }
    if (result.classification[2].value > 0.7) {
    tft.fillScreen(TFT_RED);
    tft.setFreeFont(&FreeSansBoldOblique12pt7b);
    tft.drawString("Circle", 20, 80);
    delay(1000);
    tft.fillScreen(TFT_WHITE);
    classification_flag = 2;
    }
    ....
  • Data send code block:

    With the Lora network available, we can use the function to send the tag to Helium and recover it by Decoder we wrote in the Helium.

    if (is_exist){
    int ret = 0;
    if (is_join){
    ret = at_send_check_response("+JOIN: Network joined", 12000, "AT+JOIN\r\n");
    if (ret){
    is_join = false;
    }
    else{
    Serial.println("");
    Serial.print("JOIN failed!\r\n\r\n");
    delay(5000);
    }
    }
    else{
    char cmd[128];
    sprintf(cmd, "AT+CMSGHEX=\"%08X %08X\"\r\n", classification_flag); // Change classification_flag to data wanna transfer
    ret = at_send_check_response("Done", 10000, cmd);
    if (ret){
    Serial.print("classification_flag:");s
    Serial.print(classification_flag);
    Serial.print("\t");
    recv_prase(recv_buf);
    }
    else{
    Serial.print("Send failed!\r\n\r\n");
    }
    delay(5000);
    }
    }
    else
    {
    delay(500);
    }

Tech Support & Product Discussion

Thank you for choosing our products! We are here to provide you with different support to ensure that your experience with our products is as smooth as possible. We offer several communication channels to cater to different preferences and needs.

Loading Comments...