Skip to content

Instantly share code, notes, and snippets.

@JasonBSteele
Created July 11, 2020 17:11
Show Gist options
  • Select an option

  • Save JasonBSteele/024b05a13ea4387a20f6d65ee6f11dfa to your computer and use it in GitHub Desktop.

Select an option

Save JasonBSteele/024b05a13ea4387a20f6d65ee6f11dfa to your computer and use it in GitHub Desktop.

Revisions

  1. JasonBSteele created this gist Jul 11, 2020.
    363 changes: 363 additions & 0 deletions kitchen-sounds.ino
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,363 @@
    /* Edge Impulse Arduino examples
    * Copyright (c) 2020 EdgeImpulse Inc.
    *
    * Permission is hereby granted, free of charge, to any person obtaining a copy
    * of this software and associated documentation files (the "Software"), to deal
    * in the Software without restriction, including without limitation the rights
    * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    * copies of the Software, and to permit persons to whom the Software is
    * furnished to do so, subject to the following conditions:
    *
    * The above copyright notice and this permission notice shall be included in
    * all copies or substantial portions of the Software.
    *
    * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    * SOFTWARE.
    */

    // If your target is limited in memory remove this macro to save 10K RAM
    #define EIDSP_QUANTIZE_FILTERBANK 0

    #define MINIMUM_CERTAINTY 0.75
    #define BLE_CONNECTING_DELAY 10000
    #define INFERRENCING_DELAY 2000
    #define REPEAT_NOTIFY_DELAY 600000 //10 Minutes

    /* Includes ---------------------------------------------------------------- */
    #include <PDM.h> // Microphone
    #include <ArduinoBLE.h> // Bluetooth
    #include <kitchen-sounds_inference.h> // Trained model

    /** Audio buffers, pointers and selectors */
    typedef struct {
    int16_t *buffer;
    uint8_t buf_ready;
    uint32_t buf_count;
    uint32_t n_samples;
    } inference_t;

    enum State {
    waitingForConnection,
    connecting,
    inferrencing
    };

    static const char NOISE[] = "noise";
    static char classification[20];
    static char prevClassification[sizeof(classification)];
    static unsigned long prevMillis;
    static unsigned long currentMillis;
    static unsigned long prevNotifyMillis;
    static State state = waitingForConnection;

    static inference_t inference;
    static bool record_ready = false;
    static signed short sampleBuffer[2048];
    static bool debug_nn = false; // Set this to true to see e.g. features generated from the raw signal

    BLEDevice bleCentral;
    BLEService soundsService("180C"); // User defined service

    BLEStringCharacteristic soundCharacteristic("2A56", // standard 16-bit characteristic UUID
    BLERead | BLENotify, 20); // remote clients will only be able to read this

    /**
    * @brief Arduino setup function
    */
    void setup()
    {
    for (size_t ix = 0; ix < 20; ix++) {
    ei_printf("waiting... %lu\n", ix);
    delay(1000);
    }

    //Serial.begin(115200);
    Serial.begin(9600);
    ei_printf("Edge Impulse Inferencing Demo\n");

    // summary of inferencing settings (from model_metadata.h)
    ei_printf("Inferencing settings:\n");
    ei_printf("\tInterval: %.2f ms.\n", (float)EI_CLASSIFIER_INTERVAL_MS);
    ei_printf("\tFrame size: %d\n", EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
    ei_printf("\tSample length: %d ms.\n", EI_CLASSIFIER_RAW_SAMPLE_COUNT / 16);
    ei_printf("\tNo. of classes: %d\n", sizeof(ei_classifier_inferencing_categories) / sizeof(ei_classifier_inferencing_categories[0]));

    if (microphone_inference_start(EI_CLASSIFIER_RAW_SAMPLE_COUNT) == false) {
    ei_printf("ERR: Failed to setup audio sampling\r\n");
    return;
    }

    setupBLE();
    }

    void setupBLE()
    {

    if (!BLE.begin()) { // initialize BLE
    ei_printf("starting BLE failed!");
    while (1);
    }

    BLE.debug(Serial);

    BLE.setLocalName("Kitchen Sounds"); // Set name for connection
    BLE.setAdvertisedService(soundsService); // Advertise service
    soundsService.addCharacteristic(soundCharacteristic); // Add characteristic to service
    BLE.addService(soundsService); // Add service

    BLE.advertise(); // Start advertising
    ei_printf("Peripheral device MAC: ");
    Serial.println(BLE.address());
    ei_printf("Waiting for connection...\n");
    }


    /**
    * @brief Arduino main function. Runs the inferencing loop.
    */

    void loop()
    {
    currentMillis = millis();

    switch(state) {
    case waitingForConnection:
    bleCentral = BLE.central(); // Wait for a BLE central to connect

    // if a central is connected to the peripheral:
    if (bleCentral) {
    Serial.print("Connected to central MAC: ");
    Serial.println(bleCentral.address());
    // turn on the LED to indicate the connection
    digitalWrite(LED_BUILTIN, HIGH);

    // Transition to connecting state
    prevMillis = millis();
    state = connecting;
    ei_printf("Wait 10 secs to finish BLE exchange\n");
    }
    break;

    case connecting:
    if(bleCentral && bleCentral.connected()) {
    if (currentMillis - prevMillis > BLE_CONNECTING_DELAY) {
    // Transition to inferrencing state
    prevMillis = currentMillis;
    state = inferrencing;
    ei_printf("Starting inferencing in 2 seconds...\n");
    }
    }
    else {
    disconnected();
    }
    break;

    case inferrencing:
    if (bleCentral && bleCentral.connected()) {
    if (currentMillis - prevMillis > INFERRENCING_DELAY) {
    do_inferencing();
    currentMillis = millis();

    if (strlen(classification) > 0) {
    ei_printf("Classification: %s\n", classification);

    // If 10mins since last notify or classification is different
    if (!prevNotifyMillis || currentMillis - prevNotifyMillis > REPEAT_NOTIFY_DELAY
    || strcmp(classification, prevClassification) != 0) {
    strcpy(prevClassification, classification);
    prevNotifyMillis = currentMillis;

    ei_printf("BLE Notify classification\n");
    soundCharacteristic.setValue(classification);
    }
    }

    prevMillis = currentMillis;
    ei_printf("Starting inferencing in 2 seconds...\n");
    }
    }
    else {
    disconnected();
    }

    break;
    }
    }

    void disconnected()
    {
    // when the central disconnects, turn off the LED:
    digitalWrite(LED_BUILTIN, LOW);
    ei_printf("Disconnected from central MAC: ");
    Serial.println(bleCentral.address());
    ei_printf("Waiting for connection...\n");

    //Transition to waitingForConnection state
    state = waitingForConnection;
    }

    void do_inferencing()
    {
    classification[0] = '\0';
    ei_printf("Recording...\n");

    bool m = microphone_inference_record();
    if (!m) {
    ei_printf("ERR: Failed to record audio...\n");
    return;
    }

    ei_printf("Recording done\n");

    signal_t signal;
    signal.total_length = EI_CLASSIFIER_RAW_SAMPLE_COUNT;
    signal.get_data = &microphone_audio_signal_get_data;
    ei_impulse_result_t result = { 0 };

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

    // print the predictions
    ei_printf("Predictions (DSP: %d ms., Classification: %d ms., Anomaly: %d ms.): \n",
    result.timing.dsp, result.timing.classification, result.timing.anomaly);

    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 (strcmp(result.classification[ix].label, NOISE) != 0 && result.classification[ix].value >= MINIMUM_CERTAINTY) {
    strncpy(classification, result.classification[ix].label, sizeof(classification)-1);
    }
    }
    #if EI_CLASSIFIER_HAS_ANOMALY == 1
    ei_printf(" anomaly score: %.3f\n", result.anomaly);
    #endif

    }

    /**
    * @brief Printf function uses vsnprintf and output using Arduino Serial
    *
    * @param[in] format Variable argument list
    */
    void ei_printf(const char *format, ...) {
    static char print_buf[1024] = { 0 };

    va_list args;
    va_start(args, format);
    int r = vsnprintf(print_buf, sizeof(print_buf), format, args);
    va_end(args);

    if (r > 0) {
    Serial.write(print_buf);
    }
    }

    /**
    * @brief PDM buffer full callback
    * Get data and call audio thread callback
    */
    static void pdm_data_ready_inference_callback(void)
    {
    int bytesAvailable = PDM.available();

    // read into the sample buffer
    int bytesRead = PDM.read((char *)&sampleBuffer[0], bytesAvailable);

    if (record_ready == true || inference.buf_ready == 1) {
    for(int i = 0; i < bytesRead>>1; i++) {
    inference.buffer[inference.buf_count++] = sampleBuffer[i];

    if(inference.buf_count >= inference.n_samples) {
    inference.buf_count = 0;
    inference.buf_ready = 1;
    }
    }
    }
    }

    /**
    * @brief Init inferencing struct and setup/start PDM
    *
    * @param[in] n_samples The n samples
    *
    * @return { description_of_the_return_value }
    */
    static bool microphone_inference_start(uint32_t n_samples)
    {
    inference.buffer = (int16_t *)malloc(n_samples * sizeof(int16_t));

    if(inference.buffer == NULL) {
    return false;
    }

    inference.buf_count = 0;
    inference.n_samples = n_samples;
    inference.buf_ready = 0;

    // configure the data receive callback
    PDM.onReceive(&pdm_data_ready_inference_callback);

    // optionally set the gain, defaults to 20
    PDM.setGain(80);

    //ei_printf("Sector size: %d nblocks: %d\r\n", ei_nano_fs_get_block_size(), n_sample_blocks);
    PDM.setBufferSize(4096);

    // initialize PDM with:
    // - one channel (mono mode)
    // - a 16 kHz sample rate
    if (!PDM.begin(1, EI_CLASSIFIER_FREQUENCY)) {
    ei_printf("Failed to start PDM!");
    }

    record_ready = true;

    return true;
    }

    /**
    * @brief Wait on new data
    *
    * @return True when finished
    */
    static bool microphone_inference_record(void)
    {
    inference.buf_ready = 0;
    inference.buf_count = 0;

    while(inference.buf_ready == 0) {
    delay(10);
    }

    return true;
    }

    /**
    * Get raw audio signal data
    */
    static int microphone_audio_signal_get_data(size_t offset, size_t length, float *out_ptr)
    {
    arm_q15_to_float(&inference.buffer[offset], out_ptr, length);

    return 0;
    }

    /**
    * @brief Stop PDM and release buffers
    */
    static void microphone_inference_end(void)
    {
    PDM.end();
    free(inference.buffer);
    }

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