LaraDomen HaukoGašper AzinovičPhenixTBJLuka Mali
Published

Aqua Scope

An underwater time-of-flight sensor that can detect and count fish and other objects.

IntermediateFull instructions provided15 hours131

Things used in this project

Hardware components

pool buoy for chlorine 17.8 cm
×1
Time-Of-Flight VL53L7C
×1
Glass Sheet: Exolon GP Polycarbonate Clear 09
×1
Calibri IoT NatureGuard
×1

Software apps and online services

Arduino IDE
Arduino IDE
Edge Impulse Studio
Edge Impulse Studio

Story

Read more

Code

VL53L7CX_EdgeImpulse_LoRaWAN_Uplink.ino

Arduino
VL53L7CX ToF sensor: captures 8×8 depth data, runs Edge Impulse ML inference, and transmits classification results over LoRaWAN every 2 seconds.
/*************************************************************************
 * VL53L7CX 8×8  →  Edge Impulse INT8 model  →  LoRa WAN uplink
 * Inference cadence: 1 frame every 2 seconds
 * Tested on Arduino Nano 33 BLE Sense + generic AT-command LoRa module
 *************************************************************************/
#include <Arduino.h>
#include <Wire.h>
#include <vl53l7cx_class.h>
#include <airScope_inferencing.h>

/* ---------- User switches ----------------------------------- */
#define DEBUG_DEPTH  true             // set false to silence raw dumps
#define MAX_DEPTH_MM 1024             // clip range for 255-0 mapping
#define INF_PERIOD_MS 2000            // 1 inference every 2 s

/* ---------- Pins, Serial, LoRa ------------------------------ */
#define LPN_PIN      8
#define I2C_RST_PIN  7
#define PWREN_PIN    9
#define BAUD_USB     115200

// LoRa module connected to hardware UART1 (TX-1 = D1, RX-0 = D0)
#define LORA_BAUD    9600
#define LORA_PWR_PIN 5                // pull HIGH to power the modem

/* ---------- Globals ----------------------------------------- */
VL53L7CX sensor(&Wire, LPN_PIN, I2C_RST_PIN);
VL53L7CX_ResultsData frame;

static uint8_t ei_image_buffer[EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE]; // 64 B

/* ---------- Prototypes -------------------------------------- */
bool  setup_tof_sensor(void);
bool  capture_tof_data(uint8_t *depth_buffer);
int   get_data_from_buffer(size_t offset, size_t length, float *out_ptr);
void  print_depth_mm(const VL53L7CX_ResultsData &frm);
void  print_depth_gray(const uint8_t *buf);
void  lorawan_init(void);
void  lorawan_send(const String &payload);

/* ============================================================ */
void setup() {
    /* -------- USB serial for debug -------- */
    Serial.begin(BAUD_USB);
    while (!Serial);
    Serial.println("VL53L7CX 8×8 → Edge Impulse → LoRaWAN (2 s cadence)");

    /* -------- Power & I2C for ToF --------- */
    pinMode(PWREN_PIN, OUTPUT);
    digitalWrite(PWREN_PIN, HIGH);
    delay(10);

    Wire.begin();
    Wire.setClock(400000);

    if (!setup_tof_sensor()) {
        Serial.println("Sensor init failed – halt");
        for (;;);
    }

    ei_printf("Model: %dx%d, %d bytes\n",
              EI_CLASSIFIER_INPUT_WIDTH,
              EI_CLASSIFIER_INPUT_HEIGHT,
              EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);

    /* -------- LoRa WAN modem ------------ */
    pinMode(LORA_PWR_PIN, OUTPUT);
    digitalWrite(LORA_PWR_PIN, HIGH);       // turn on the modem
    delay(100);                             // give it time to boot
    Serial1.begin(LORA_BAUD);
    lorawan_init();
}

/* ============================================================ */
void loop() {

    static uint32_t last_inf = 0;
    if (millis() - last_inf < INF_PERIOD_MS) return;   // pace loop

    uint8_t depth_buf[64];
    if (!capture_tof_data(depth_buf)) {
        Serial.println("Depth capture failed");
        return;
    }

    /* optional dump ------------------------------------------------ */
    if (DEBUG_DEPTH) {
        print_depth_mm(frame);
        print_depth_gray(depth_buf);
    }

    /* copy depth → EI buffer (uint8, no scaling) ------------------ */
    memcpy(ei_image_buffer, depth_buf, 64);

    /* create Edge Impulse signal ---------------------------------- */
    ei::signal_t signal;
    signal.total_length = EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE; // 64
    signal.get_data     = &get_data_from_buffer;

    ei_impulse_result_t res = { 0 };
    EI_IMPULSE_ERROR err = run_classifier(&signal, &res, /*debug*/false);
    if (err != EI_IMPULSE_OK) {
        ei_printf("run_classifier error %d\n", err);
        return;
    }

    ei_printf("Predictions (DSP %d ms, CLS %d ms):\n",
              res.timing.dsp, res.timing.classification);

    /* find best class ------------------------------------------------ */
    uint16_t best_idx = 0;
    float    best_val = res.classification[0].value;
    for (uint16_t i = 1; i < EI_CLASSIFIER_LABEL_COUNT; ++i) {
        if (res.classification[i].value > best_val) {
            best_val = res.classification[i].value;
            best_idx = i;
        }
        ei_printf("  %s: %.5f\n",
                  ei_classifier_inferencing_categories[i],
                  res.classification[i].value);
    }

    /* ---------- build & send LoRa payload --------------- */
    String payload = "{\"";
    payload += ei_classifier_inferencing_categories[best_idx];
    payload += "\": ";
    payload += String(best_val, 5);
    payload += "}";

    lorawan_send(payload);

    last_inf = millis();
}

/* ============================================================ */
/*                 Sensor helpers & callbacks                    */
/* ============================================================ */
bool setup_tof_sensor() {
    return !(sensor.begin() ||
             sensor.init_sensor() ||
             sensor.vl53l7cx_set_resolution(VL53L7CX_RESOLUTION_8X8) ||
             sensor.vl53l7cx_set_ranging_frequency_hz(5) ||        // 5 Hz
             sensor.vl53l7cx_start_ranging());
}

/* Robust 8×8 capture – restarts ranging if timeout ------------- */
bool capture_tof_data(uint8_t *depth_buffer) {

    uint32_t t0 = millis();
    while (true) {
        uint8_t ready = 0;
        if (sensor.vl53l7cx_check_data_ready(&ready)) break;
        if (ready) break;
        if (millis() - t0 > 300) {                 // 300 ms timeout
            Serial.println("Timeout – restart ranging");
            sensor.vl53l7cx_stop_ranging();
            sensor.vl53l7cx_start_ranging();
            return false;
        }
        delayMicroseconds(300);
    }

    if (sensor.vl53l7cx_get_ranging_data(&frame)) {
        Serial.println("get_ranging_data() failed");
        return false;
    }

    for (uint8_t i = 0; i < 64; ++i) {
        uint16_t d = frame.distance_mm[i];
        if (frame.nb_target_detected[i] == 0)        depth_buffer[i] = 0;
        else if (d > MAX_DEPTH_MM)                   depth_buffer[i] = 255;
        else                                         depth_buffer[i] =
                           255 - (uint8_t)((d * 255) / MAX_DEPTH_MM);
    }
    return true;
}

/* Provide raw uint8 → float for INT8 model --------------------- */
int get_data_from_buffer(size_t offset, size_t length, float *out) {
    for (size_t i = 0; i < length; ++i)
        out[i] = static_cast<float>(ei_image_buffer[offset + i]);
    return 0;
}

/* ---------------- Debug helpers ------------------------------ */
void print_depth_mm(const VL53L7CX_ResultsData &frm) {
    Serial.println("▼ distance (mm)");
    for (uint8_t y = 0; y < 8; ++y) {
        for (uint8_t x = 0; x < 8; ++x) {
            uint16_t d = frm.nb_target_detected[y*8+x] ?
                          frm.distance_mm[y*8+x] : 0;
            Serial.print(d); Serial.print(x<7?'\t':'\n');
        }
    }
}

void print_depth_gray(const uint8_t *buf) {
    Serial.println("▼ grayscale (0-255)");
    for (uint8_t y = 0; y < 8; ++y) {
        for (uint8_t x = 0; x < 8; ++x) {
            Serial.print(buf[y*8+x]); Serial.print(x<7?'\t':'\n');
        }
    }
    Serial.println();
}

/* ============================================================ */
/*                       LoRa WAN helpers                        */
/* ============================================================ */
void lorawan_init() {
    /* Adjust the AT commands for your specific modem / region  */
    const char *cmds[] = {
        "AT+ID=AppEui,\"1111111111111111\"",
        "AT+ID=DevEui,\"70B3D57ED007083A\"",
        "AT+KEY=AppKey,\"893AFF6D1366FD19E024865515FB90CA\"",
        "AT+MODE=LWOTAA",
        "AT+JOIN"
    };
    for (uint8_t i = 0; i < sizeof(cmds)/sizeof(cmds[0]); ++i) {
        Serial1.print(cmds[i]); Serial1.print("\r\n");
        delay(1000);
    }
    Serial.println("LoRa WAN join sequence sent");
}

/* Send one JSON record over LoRa - plain text ------------------ */
void lorawan_send(const String &payload) {
    Serial.print("LoRa payload: "); Serial.println(payload);

    Serial1.print("AT+MSG=\"");
    Serial1.print(payload);
    Serial1.println("\"");
    /* On most modules a positive "Done" or "OK" comes back ~2-3 s later.
       For production code you may want to parse that response and retry. */
}

Credits

Lara
1 project • 1 follower
Domen Hauko
1 project • 1 follower
Gašper Azinovič
1 project • 2 followers
PhenixTBJ
1 project • 1 follower
Luka Mali
20 projects • 24 followers
Maker Pro, prototyping enthusiast, head of MakerLab, a lecturer at the University of Ljubljana, founder.

Comments