diff --git a/README.md b/README.md index b1d40c9..736c384 100644 --- a/README.md +++ b/README.md @@ -3,118 +3,128 @@ * [General](#General) * [Electronic components assembly](#electronic-components-assembly) -* [Board configuration](#Board-configuration) -* [Libraries](#Libraries) +* [Software Build](#Software-Build) * [Config file](#Config-file) * [Specification](#Specification) +* [Test Report](#Test-Report) +* [TTN message interface](#TTN-message-interface) * [Example graphical output Sound Kit](#Example-graphical-output-Sound-Kit) +* [Last Updates](#Last-Updates) +* [To Do](#To-Do) ## General +This Soundkit is easy to build for a low cost price (ca. 40 euro) and is therefore very suitable to be used for citizen sensing. -This Soundkit sensor measures continuously audible sound by analyzing the data using FFT. The results are send each minute to the LoRa network. The sensor measures audible spectrum from 31.5 Hz to 8 kHz divided in 9 octaves. Also each minute the average, minimum and maximum levels are calculated for the 3 weighting curves dB(A), dB(C) and db(Z). +This sensor measures continuously audible sound by analyzing the data using FFT process. The measured results are send in a report at regular time intervals to the LoRaWan network, where it can be picked up for visualization. -Sound Kit Sparkfun board +No adjustments are needed, because the sensor uses a digital MEMS microphone. which is factory calibrated. Furthermore it can easily be connected to the community network, viz. it is independent of a home or office network. -> Sound Kit Sparkfun board +The sensor make use of the powerful ESP32 processor, which is a dual core processor. One core handles continuously the processing of audio, while the other core handles the LoRaWan communication. -Sound Kit TTGO board +The sensor measures audible spectrum from 31.5 Hz to 8 kHz divided in 9 octaves. Each regular time interval the average, minimum and maximum levels are calculated for the 3 weighting curves dB(A), dB(C) and db(Z). -> Sound Kit TTGO board - -Sound Kit TTGO OLED display +Sound Kit TTGO board > Sound Kit TTGO OLED display +Several MEMS microphones can be used, the Soundkit is tested with SPH0645 and ICS43434. +The ICS43434 is adviced by Sensor.community, because the small headerboard can be assembled in a half inch pipe (13mm) fillled with resin in order to minimize audio resonance. Sound calibrators uses also this diameter size. + +ICS43434 microphone + +> ICS43434 in pipe filled with resin + ## Electronic components assembly -The software is based on ESP32 processor with Lora module. Two boards has been tested viz. Sparkfun LoRa board and TTGO LoRa board. +The software is based on TTGO LoRa T3 board, this board includes a ESP32 processor, a Lora RFM95 module, LoRa antenna. The TTGO LoRa board does have an OLED display, and will display the average dB(A), dB(C) and dB(z) levels. -#### Components -* Sparkfun LoRa Gateway 1-channel ESP32 (used as Sensor), or TTGO LoRa32 V1 -* I2S MEMS microphone SPH046 or I2S MEMS microphone NMP441 -* antenna ¼ lambda, e.g a wire of 8.4 cm length or 868MHz Helical Antenna -* power adapter 5V, 0.5A - -The table below shows the wiring between MEMS microphone (SPH0645 or NMP443) and the processor board (Sparkfun or TTGO): -| SPH0645 | NMP442 | |Sparkfun| TTGO | -| ------- | ------ |--|--------|-------| -| 3V | 3V | <--> | 3V | 3V | -| GND | GND | <--> | GND | GND| -| BCLK | SCK | <--> | GPIO18 | GPIO13 | -| DOUT | SD | <--> | GPIO19 | GPIO35 | -| LRCL | WS | <--> | GPIO23 | GPIO12 | -| | LR | GND | | | -| SEL | | not connected | | | - -#### N.B. -For sound measurements lower then 30 dB, the supply to the MEMS microphone must be very clean. The 3V supplied by the Sparkfun ESP gives in my situation some rumble in low frequencies. It can be uncoupled by extra 100nf and 100 uF or a separate 3.3V stabilzer. - -## Board configuration in PlatformIO - -Choose your board in the platformio.ini file and change "default_envs" in one of the two lines below: -* default_envs = ttgo -* default_envs = sparkfun +Several MEMS microphones can be connected SPH0645 or ICS43434 +The SPH0645 was slightly better in low frequencies. This is more important if you want to measure dB(C) and dB(Z) levels. +The ICS43434 is better in low levels, less noise. -## Board configuration in Arduino - -Install ESP32 Arduino Core -Add the line in Arduino→preferences→Additional Boardsmanagers URL: +#### Components +- Processor board: LilyGO TTGO T3 LoRa32 +- I2S Microphone: SPH0645 or ICS43434 +- power adapter 5V, 0.5A + +The table below shows the wiring between MEMS microphone (SPH0645 or ICS43434) and the processor board TTGO: +| SPH0645 | ICS43434| | TTGO T3| +| ------- | ------ | ------ |--------| +| 3V | 3V | <--> | 3V | +| GND | GND | <--> | GND | +| BCLK | SCK | <--> | GPIO00 | +| DOUT | SD | <--> | GPIO35 | +| LRCL | WS | <--> | GPIO12 | +| | LR | | GND | +| SEL | | |not connected| | + +**Remark 1:** + +The length of the wires between MEMS and TTGO-board should not exceed 15 cm and tie the wires close to each other, or use a 5 wire flat cable. + +**Remark 2** + +For sound measurements lower then 30 dB, the power to the MEMS microphone must be very clean. The 3.3V supplied to the MEMS can cause some rumble in low frequencies. It can be improved by placing an extra 100nf and 100 uF capacitor in parallel. + +## Software Build +The sources are developed and compiled by Visual Code with the PlatformIO. +### Board configuration +The platformio.ini file is configured for the LilyGO TTGO T3 LoRa32 board. ``` - https://dl.espressif.com/dl/package_esp32_index.json +[env:ttgo-lora32-v21] ``` -Restart Arduino environment. - -In the Arduino menu Tools→Boards, choose your board you want to use: -* Select TTGO LoRa32-OLED V1 board or -* Select Sparkfun LoRa Gateway 1-Channel board - -The Sounkit sourcecode supports both boards. If Sparkfun Lora gateway is not vissible check the presence of the Sparkfun variant file, see instructions at https://learn.sparkfun.com/tutorials/sparkfun-lora-gateway-1-channel-hookup-guide/programming-the-esp32-with-arduino
- - -## Libraries - -If you develop in PlatformIO, you can skip this section, libraries and macros are defined in platformio.ini and are installed automatically. +### Libraries +Libraries are installed automatically. The macros and libs are defined in platformio.ini. The following libraries are used in this project: +- espressif32@3.5.0 +- mcci-catena/MCCI LoRaWAN LMIC library@ 4.1.1, +- adafruit/Adafruit GFX Library and adafruit/Adafruit SSD1306 #### LMIC -There are several LIMC LoRaWan libraries. I use the LMIC library from MCCI-Catena, because this one is currently best maintained. -Download the library from https://github.com/mcci-catena/arduino-lmic and put it in your [arduino-path]\libraries\ - -Take care that you change the frequency plan to Europe (if you are in Europe), because it is defaulted to the US. It can be changed in the file [arduino-path]\arduino-lmic-master\project_config\lmic_project_config.h +The frequency plan in MCCI LoRaWAN LMIC library is defaulted to US. In the platformio.ini it is overruled to Europe by the build flags: ``` -#define CFG_eu868 1 -``` - -#### Monochrome OLED library -For the TTGO board, download the libraries below and put them in your [Arduino-path]\libraries -``` -https://github.com/adafruit/Adafruit_SSD1306 -https://github.com/adafruit/Adafruit-GFX-Library +build_flags = + -D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS + -D CFG_eu868=1 + -D CFG_sx1276_radio=1 ``` #### Arduino FFT I used the https://www.arduinolibraries.info/libraries/arduino-fft library. -The two files “arduinoFFT.h” and arduinoFFT.ccp” are already in your .ino main directory +The two files “arduinoFFT.h” and arduinoFFT.ccp” are already present in your source directory. ## Config file In the config.h some parameters are defined. -#### Cycle count -The cycle count defines how often a measurement is sent to the thingsnetwork in msec.: +#### CycleTime +The cycle count defines how often a measurement is sent to the thingsnetwork in seconds: +``` +#define CYCLETIME 60 +``` +For RIVM and sensor.community use 150 + +Set microphone dependent correction in dB ``` -#define CYCLECOUNT 60000 +#define MIC_OFFSET 0.0 ``` +for SPH0645 use -1.8
+for ICS43434 use 1.5
+otherwise use 0.0 + + #### LoRa TTN keys -The device address and keys have to be set to the ones generated in the TTN console. Login in the TTN console and add your device. -Choose activation mode OTAA and copy the APPEUI, DEVEUI and APPKEY keys into this config file: +TTN V2 stops at the end of 2021, so my advice is use the TTN console V3 to set your keys. +Register your device, choose 'manually' and MAC version 1.03. +Choose activation mode OTAA and copy the the (generated) APPEUI and APPKEY keys into this config file (config,h): ``` #define APPEUI "0000000000000000" -#define DEVEUI "0000000000000000" #define APPKEY "00000000000000000000000000000000" ``` +Note that unique DEVEUI is read from the TTGO ESP32 board id. The DEVEUI is displayed in the TTGO log during startup and is displayed on the OLED display. This value must be entered in the TTN console in the field DEVEUI. ## Specification -#### Sound Measuerment -* sample frequency MEMS microphone 22.628 khz +#### Sound Measurement +* Accuracy < 1 dB +* sample frequency MEMS microphone 22.628 kHz * 18 bits per sample * soundbuffer 2048 samples * FFT bands in bins of 11 Hz (22628 / 2048) @@ -123,8 +133,18 @@ Choose activation mode OTAA and copy the APPEUI, DEVEUI and APPKEY keys into thi * spectrum 31.5Hz, 63Hz, 123Hz, 250Hz, 500 Hz, 1kHz, 2kHz, 4kHz and 8kHz * average, maximum and minimum level -#### Interface -Every minute a message is composed from all measurements done in one minute, which contains the following values in dB. +## Test Report +The [test report can be found HERE](test/Test-report-TTGO-LoRa-Soundkit.pdf) +The test report desribes the tests of the Soundkit together with the Teensy4 soundsensor of the Sensor Community Germany. +Three test sets are used and the results are compared. The sets are: +- Soundkit LoRa TTGO with ICS43434 +- Soundkit LoRa TTGO with SPH0645 +- Soundsensor Teensy4 of the Sensor Community Germany. + +The tests are executed with certified class 1 calibrator and soundlevel meter. + +## TTN message interface +Every interval time (e.g. each 2 minutes) a message is composed from all measurements done in one interval, which contains the following values in dB. * 9 spectrum levels for dB(A) * 9 spectrum levels for dB(C) * 9 spectrum levels for dB(Z) @@ -132,9 +152,9 @@ Every minute a message is composed from all measurements done in one minute, whi * min, max, and average levels for dB(C) * min, max, and average levels for dB(Z) -The message is send in a compressed binary format to TTN. The TTN payload decoder converts the messsage to a readable JSON message. +The message is send in a compressed binary format to TTN. The TTN payload decoder converts the message to a readable JSON message. -#### Example of a JSON message: +### Example of a JSON message: ``` "la": { "avg": 44.2, @@ -187,15 +207,34 @@ The message is send in a compressed binary format to TTN. The TTN payload decode } ``` ## Example graphical output Sound Kit -Below a graph of a sound measurement in my living room in dB(A). +Below a heatmap and graph of a sound measurement on my balcony in quiet residential area. The values shown are in dB(Z) In this graph some remarkable items are vissible: -* blue line shows the max level of the belling comtoise clock each half hour -* visible noise of the dishing machine from 0:30 to 1:30 -* noise of of the fridge the 125 Hz line -* incrementing outside traffic (63 Hz) at 7.00 +* birds at 4khz at sunrise and sunset +* jet airplane at 250 Hz +* morning rush (traffic) at 63 Hz +Note that db(Z) levels shows a lot of noise in low frequencies, which is mostly not vissible in common db(A) measuremnts. ![alt Example output](images/grafana.png "Example output") -The green blocks shows the average spectrum levels. This graph is made with Nodered, InfluxDb and Grafana. +## Last Updates +Changed 18-3-2024 +- Sources adapted for LilyGO TTGO T3 LoRa32 board (10dB better radio performance) +- Sources adapted for VC PlatformIO (arduino IDE support not tested) +- During a TTN connect phase, the I2S MEMS driver is stopped, because it gives Rx radio interference +- The update of the OLED display is moved from the sound thread to the main thread. + +Changed 15-8-2021 + - MCCI Catena LoRa stack + - Worker loop changed (hang situation solved with TTN V3) + - OLED display added + - DEVEUI obtained from BoardID, (same SW for multiple sensors) + - use the TWO processor cores of ESP (one core for audio and one core for LoRa) + - DC offset MEMS compensated by moving average window + - payload compressed from 27 to 19 bytes + - test report added + +## To Do +Advice for sensor housing and microphone placement. + diff --git a/images/grafana.png b/images/grafana.png index 5362d6b..654cf3b 100644 Binary files a/images/grafana.png and b/images/grafana.png differ diff --git a/images/ics43434.jpg b/images/ics43434.jpg new file mode 100644 index 0000000..e82c0c0 Binary files /dev/null and b/images/ics43434.jpg differ diff --git a/images/ics43434.png b/images/ics43434.png new file mode 100644 index 0000000..8a960a1 Binary files /dev/null and b/images/ics43434.png differ diff --git a/images/ttgo.jpg b/images/ttgo.jpg index d682e62..9c82ab9 100644 Binary files a/images/ttgo.jpg and b/images/ttgo.jpg differ diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..88fce22 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,32 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:ttgo-lora32-v21] +; platform = espressif32 +platform = espressif32@3.5.0 +board = ttgo-lora32-v21 +framework = arduino +lib_deps = + mcci-catena/MCCI LoRaWAN LMIC library@^4.1.1 + adafruit/Adafruit GFX Library + adafruit/Adafruit SSD1306 + +build_flags = + -D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS + -D CFG_eu868=1 + -D CFG_sx1276_radio=1 + +monitor_speed = 115200 + +; change microcontroller +board_build.mcu = esp32 + +; change MCU frequency +board_build.f_cpu = 240000000L diff --git a/release.txt b/release.txt index 666fea2..61b99d8 100644 --- a/release.txt +++ b/release.txt @@ -1,3 +1,19 @@ +Changes in Version 4 +- Sources adapted for LilyGO TTGO T3 LoRa32 board (10dB better radio performance) +- Sources adapted for VC PlatformIO (arduino IDE support not tested) +- During a TTN connect phase, the I2S MEMS driver is stopped, because it gives Rx radio interference +- The update of the OLED display is moved from the sound thread to the main thread. + +Changes in Version 3 +- Test report added +- MCCI Catena LoRa stack +- Worker loop changed (hang situation solved with TTN V3) +- OLED display added +- DEVEUI obtained from BoardID, (same SW for multiple sensors) +- use the TWO processor cores of ESP (one core for audio and one core for LoRa) +- DC offset MEMS compensated by moving average window +- payload compressed from 27 to 19 bytes + Changes in Version 2 - update Readme, TTGO added, pinning diagram for different configurations, interface changed - config.h, contains all relevant parameters diff --git a/src/lorasoundkit/arduinoFFT.cpp b/src/arduinoFFT.cpp similarity index 100% rename from src/lorasoundkit/arduinoFFT.cpp rename to src/arduinoFFT.cpp diff --git a/src/lorasoundkit/arduinoFFT.h b/src/arduinoFFT.h similarity index 100% rename from src/lorasoundkit/arduinoFFT.h rename to src/arduinoFFT.h diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..6cd952f --- /dev/null +++ b/src/config.h @@ -0,0 +1,27 @@ +/** + * Define here your configuration + * - cycletime + * - LoRa TTN keys + * Marcel Meek, May 2020 + * + */ + +#ifndef _CONFIG_h /* Prevent loading library twice */ +#define _CONFIG_h + +// define in seconds, how often a message will be sent +#define CYCLETIME 120 // use 150 for RIVM and sensor.community project + +// set microphone dependent correction in dB +// for SPH0645 define -1.8 +// for ICS43434 define 1.5 +// otherwise define 0.0 +#define MIC_OFFSET 1.5 + +// specify here TTN keys + +#define APPEUI "70B3D57ED003ED46" +#define APPKEY "53A4419ACE4EABF7BE9E5FB0B8A16F27" +// DEVEU is obtained from ESP board id + +#endif diff --git a/src/lorasoundkit/lora.cpp b/src/lora.cpp similarity index 69% rename from src/lorasoundkit/lora.cpp rename to src/lora.cpp index 9bc2dff..2a52145 100644 --- a/src/lorasoundkit/lora.cpp +++ b/src/lora.cpp @@ -25,6 +25,7 @@ * Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in * g1, 0.1% in g2), but not the TTN fair usage policy (which is probably * violated by this sketch when left running for longer)! + * To use this sketch, first register your application and device with * the things network, to set or generate an AppEUI, DevEUI and AppKey. * Multiple devices can use the same AppEUI, but each device has its own @@ -37,50 +38,46 @@ #include #include #include - #include "lora.h" -#include "config.h" -// pin mappings -#if defined(ARDUINO_TTGO_LoRa32_V1) -// define IO pins TTGO LoRa32 V1 +// forward decl. +static void parseHex( u1_t *byteBuffer, const char* str); +static void parseHexReverse( u1_t *byteBuffer, const char* str); + +// handle TTN keys +static const char *appeui, *deveui, *appkey; +extern void os_getArtEui (u1_t* buf) { parseHexReverse( buf, appeui);} +extern void os_getDevEui (u1_t* buf) { parseHexReverse( buf, deveui);} +extern void os_getDevKey (u1_t* buf) { parseHex( buf, appkey);} + +static osjob_t sendjob; +static void (*workerCallback)(void) = NULL; // worker callback +static void (*rxCallback)(unsigned int, uint8_t*, unsigned int) = NULL; // TTN receive handler +//static void (*txCompleteCallback)(bool ok) = NULL; +static bool txReady= false; + +// Schedule TX every this many seconds (might become longer due to duty +// cycle limitations). + +// Pin mapping const lmic_pinmap lmic_pins { .nss = 18, .rxtx = LMIC_UNUSED_PIN, - .rst = 14, + .rst = 23, // v1=14, v2=23 .dio = {26, 33, 32}, }; -#elif defined(ARDUINO_ESP32_DEV) -// define IO pins Sparkfun -const lmic_pinmap lmic_pins { - .nss = 16, - .rxtx = LMIC_UNUSED_PIN, - .rst = 5, - .dio = {26, 33, 32}, -}; -#else - #error Unsupported board selection. -#endif +void printHex2(unsigned v) { + v &= 0xff; + if (v < 16) + Serial.print('0'); + Serial.print(v, HEX); +} -// handle TTN keys -static void printHex( u1_t *buf, size_t len); -static void parseHex( u1_t *byteBuffer, const char* str); -static void parseHexReverse( u1_t *byteBuffer, const char* str); - -extern void os_getArtEui (u1_t* buf) { parseHexReverse( buf, APPEUI);} -extern void os_getDevEui (u1_t* buf) { parseHexReverse( buf, DEVEUI);} -extern void os_getDevKey (u1_t* buf) { parseHex( buf, APPKEY);} - -static bool busy = false; -static bool ok = false; -//static osjob_t sendjob; -static void (*rxCallback)(unsigned int, uint8_t*, unsigned int) = NULL; // TTN receive handler - -// handle events -extern void onEvent (ev_t ev) { +void onEvent (ev_t ev) { Serial.print(os_getTime()); Serial.print(": "); + txReady = false; switch(ev) { case EV_SCAN_TIMEOUT: Serial.println(F("EV_SCAN_TIMEOUT")); @@ -105,16 +102,31 @@ extern void onEvent (ev_t ev) { u1_t nwkKey[16]; u1_t artKey[16]; LMIC_getSessionKeys(&netid, &devaddr, nwkKey, artKey); - Serial.print("netid: "); Serial.println(netid, DEC); - Serial.print("devaddr: "); Serial.println(devaddr, HEX); - Serial.print("AppSKey: "); printHex( artKey, sizeof(artKey)); - Serial.print("NwkSKey: "); printHex( nwkKey, sizeof(nwkKey)); + Serial.print("netid: "); + Serial.println(netid, DEC); + Serial.print("devaddr: "); + Serial.println(devaddr, HEX); + Serial.print("AppSKey: "); + for (size_t i=0; i // uint8_t type // external functions -extern void loraBegin(); +extern void loraBegin(const char* appeui, const char* deveui, const char* appkey); extern void loraSetRxHandler( void (*callback)(unsigned int, uint8_t*, unsigned int)); +extern void loraJoin(); extern bool loraSend( int port, uint8_t* mydata, int len); - +extern bool loraConnected(); +extern bool loraTxReady(); +extern void loraSetWorker( void (*worker)( void)); +//extern void loraSetTxComplete( void (*txComplete)(bool ok)); +extern void loraSleep( int seconds); +extern void loraLoop( void); #endif // __LORA_H_ diff --git a/src/lorasoundkit/config.h b/src/lorasoundkit/config.h deleted file mode 100644 index b14ccf2..0000000 --- a/src/lorasoundkit/config.h +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Define here your configuration - * - cyclecount - * - LoRa TTN keys - * Marcel Meek, May 2020 - * - */ - -#ifndef _CONFIG_h /* Prevent loading library twice */ -#define _CONFIG_h - -// define im milleseconds how often a message will be sent -#define CYCLECOUNT 20000 //60000 - -// specify here TTN keys -//#define APPEUI "0000000000000000" -//#define DEVEUI "0000000000000000" -//#define APPKEY "00000000000000000000000000000000" - -#define APPEUI "70B3D57ED003ED46" -#define DEVEUI "0000000000000001" -#define APPKEY "80489B327CB6058C93614F5DC8187CD4" - - -#endif diff --git a/src/lorasoundkit/lorasoundkit.ino b/src/lorasoundkit/lorasoundkit.ino deleted file mode 100644 index 0114129..0000000 --- a/src/lorasoundkit/lorasoundkit.ino +++ /dev/null @@ -1,162 +0,0 @@ -/** - TTGO / Sparkfun I2S Noise FFT with LoRa - - This example calculates noise in octave bands, in a,c and z weighting - Using Arduino FFT https://www.arduinolibraries.info/libraries/arduino-fft - Using LoRa LMIC https://github.com/matthijskooijman/arduino-lmic - example: https://bitbucket.org/edboel/edboel/src/master/noise/ - - Marcel Meek, May 2020 -*/ - -#include -#include "config.h" -#include "lora.h" -#include "soundsensor.h" -#include "measurement.h" - - -#if defined(ARDUINO_TTGO_LoRa32_V1) -#include "oled.h" -static Oled oled; -#endif - -// create soundsensor -static SoundSensor soundSensor; - -// Weighting lists -static float aweighting[] = A_WEIGHTING; -static float cweighting[] = C_WEIGHTING; -static float zweighting[] = Z_WEIGHTING; - -// measurement buffers -static Measurement aMeasurement( aweighting); -static Measurement cMeasurement( cweighting); -static Measurement zMeasurement( zweighting); - -long milliCount = -1; -long cycleCount = CYCLECOUNT; -bool ttnOk = false; - -// LoRa receive handler (downnlink) -void loracallback( unsigned int port, unsigned char* msg, unsigned int len) { - printf("lora download message received port=%d len=%d\n", port, len); - - // change cycle count in seconds with a remote TTN download command - // port is 20, byte 0 is low byte 1 is high byte - if( port == 20 && len >=2) { - int value = msg[0] + 256 * msg[1]; - if( value >= 10 && value <= 600) { - cycleCount = 1000 * value; - printf( "cycleCount changed to %d sec.\n" , value); - } - } -} - -// Arduino set up -void setup(void) { - - Serial.begin(115200); - delay(100); - printf("Setup..\n"); - pinMode(LED_BUILTIN, OUTPUT); // lit if sending data - digitalWrite( LED_BUILTIN, HIGH); - -#if defined(ARDUINO_TTGO_LoRa32_V1) - oled.begin(); -#endif - soundSensor.begin(); - loraBegin(); - loraSetRxHandler( loracallback); // set LoRa receive handler (downnlink - loraSend( 0, NULL, 0); // send LoRA Join message - digitalWrite( LED_BUILTIN, LOW); - - printf("End setup\n"); -} - -// compose message, and send it to TTN -// convert shorts to 12 bit integers, to save 25% space in th TTN message -static void sendToTTN( Measurement& la, Measurement& lc, Measurement& lz) { - digitalWrite( LED_BUILTIN, HIGH); - unsigned char payload[80]; - int i = 0; // nibble count, a nibble is 4 bits - - // convert floats to 12 bits integers - - i = add12bitsToBuf( payload, i, la.min * 10.0); - i = add12bitsToBuf( payload, i, la.max * 10.0); - i = add12bitsToBuf( payload, i, la.avg * 10.0); - - i = add12bitsToBuf( payload, i, lc.min * 10.0); - i = add12bitsToBuf( payload, i, lc.max * 10.0); - i = add12bitsToBuf( payload, i, lc.avg * 10.0); - - i = add12bitsToBuf( payload, i, lz.min * 10.0); - i = add12bitsToBuf( payload, i, lz.max * 10.0); - i = add12bitsToBuf( payload, i, lz.avg * 10.0); - - // send only LZ spectrum to Lora, LA and LC is generated at the TTN server side from LZ - for ( int j = 0; j < OCTAVES; j++) { - i = add12bitsToBuf( payload, i, lz.spectrum[j] * 10.0); - } - - int len = i / 2 + (i % 2); - printf( "messagelength=%d\n", len); - - if ( len > 51) // max TTN message length - printf( "message to big length=%d\n", len); - //lora.sendMsg( 21, payload, len ); // use port 21, protocol V2 - ttnOk = loraSend( 21, payload, len ); - digitalWrite( LED_BUILTIN, LOW); -} - -// add 12 bits value to payloadbuffer -int add12bitsToBuf( unsigned char* buf, int nibbleCount, short val) { - if ( nibbleCount % 2 == 0) { - buf[ nibbleCount / 2] = val >> 4; - buf[ nibbleCount / 2 + 1] = (val << 4) & 0xF0; - } - else { - buf[ nibbleCount / 2] |= ((val >> 8) & 0x0F); - buf[ nibbleCount / 2 + 1] = val; - } - return nibbleCount + 3; -} - -// Arduino Main Loop -void loop(void) { - - // read chunk form MEMS and perform FFT, and sum energy in octave bins - float* energy = soundSensor.readSamples(); - - // update - aMeasurement.update( energy); - cMeasurement.update( energy); - zMeasurement.update( energy); - - // calculate average and send - if ( millis() - milliCount > cycleCount) { - //printf("\n"); - milliCount = millis(); - - aMeasurement.calculate(); - cMeasurement.calculate(); - zMeasurement.calculate(); - - // debug info, should be comment out - //aMeasurement.print(); - //cMeasurement.print(); - //zMeasurement.print(); - - sendToTTN( aMeasurement, cMeasurement, zMeasurement); - -#if defined(ARDUINO_TTGO_LoRa32_V1) - oled.showValues( aMeasurement.avg, cMeasurement.avg, zMeasurement.avg, ttnOk); -#endif - - // reset counters etc. - aMeasurement.reset(); - cMeasurement.reset(); - zMeasurement.reset(); - } -} diff --git a/src/lorasoundkit/oled.cpp b/src/lorasoundkit/oled.cpp deleted file mode 100644 index 5645ec1..0000000 --- a/src/lorasoundkit/oled.cpp +++ /dev/null @@ -1,58 +0,0 @@ -/******************************************************************************* -* file oled.h -* Oled wrapper, interface to display messages -* author Marcel Meek -*********************************************************************************/ - -#include "oled.h" - -//OLED pins -#define OLED_SDA 4 -#define OLED_SCL 15 -#define OLED_RST 16 -#define SCREEN_WIDTH 128 // OLED display width, in pixels -#define SCREEN_HEIGHT 64 // OLED display height, in pixels - -Oled::Oled() { - display = new Adafruit_SSD1306( SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RST); -}; - -Oled::~Oled() { - delete display; -} - -void Oled::begin() { - - //reset OLED display via software - pinMode(OLED_RST, OUTPUT); - digitalWrite(OLED_RST, LOW); - delay(20); - digitalWrite(OLED_RST, HIGH); - - //initialize OLED - Wire.begin(OLED_SDA, OLED_SCL); - if (!display->begin(SSD1306_SWITCHCAPVCC, 0x3c, false, false)) { // Address 0x3C for 128x32 - printf("SSD1306 allocation failed\n"); - for (;;); // Don't proceed, loop forever - } - - display->clearDisplay(); - //display->setRotation( 2); // rotate 180 degrees - display->setTextColor(WHITE); - display->setTextSize(2); - display->setCursor(0, 0); display->printf("Soundkit"); - display->setCursor(0, 17); display->printf("Starting"); - display->display(); -} - -void Oled::showValues( float la, float lc, float lz, bool ttnOk) { - display->clearDisplay(); - //display->setRotation( 2); // rotate 180 degrees - display->setTextColor(WHITE); - display->setTextSize(2); - display->setCursor(0, 0); display->printf("dB(A) %.1f", la); - display->setCursor(0, 17); display->printf("dB(C) %.1f", lc); - display->setCursor(0, 34); display->printf("dB(Z) %.1f", lz); - display->setCursor(0, 51); display->printf("TTN %s", (ttnOk) ? "ok" : "fail"); - display->display(); -} diff --git a/src/lorasoundkit/platformio.ini b/src/lorasoundkit/platformio.ini deleted file mode 100644 index 070b04f..0000000 --- a/src/lorasoundkit/platformio.ini +++ /dev/null @@ -1,38 +0,0 @@ -; PlatformIO Project Configuration File -; -; Build options: build flags, source filter -; Upload options: custom upload port, speed and extra flags -; Library options: dependencies, extra library storages -; Advanced options: extra scripting -; -; Please visit documentation for the other options and examples -; http://docs.platformio.org/page/projectconf.html - -[platformio] -src_dir = . -default_envs = ttgo - -[env] -platform = espressif32 -framework = arduino -monitor_speed = 115200 -upload_speed = 921600 -build_flags = - -D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS - -D CFG_eu868=1 - -D CFG_sx1276_radio=1 -lib_deps = - mcci-catena/MCCI LoRaWAN LMIC library - adafruit/Adafruit SSD1306 - adafruit/Adafruit GFX Library - adafruit/Adafruit BusIO - -[env:ttgo] -board = ttgo-lora32-v1 -build_flags = ${env.build_flags} -D ARDUINO_TTGO_LoRa32_V1 - -[env:sparkfun] -board = sparkfun_lora_gateway_1-channel -build_flags = ${env.build_flags} -D ARDUINO_ESP32_DEV - - diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..4da4b84 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,269 @@ +/*-------------------------------------------------------------------- + LoRa Soundkit + Measures environtmentalsound and send data to LoRa network + + Author Marcel Meek + Date 12/7/2020 + changed 18-3-2024 + - Sources adapted for LilyGO TTGO T3 LoRa32 board (10dB better radio performance) + - Sources adapted for VC PlatformIO (arduino IDE support not tested) + - During a TTN connect phase, the I2S MEMS driver is stopped, because it gives Rx radio interference + - The update of the OLED display is moved from the sound thread to the main thread. + changed 15-8-2021 + - MCCI Catena LoRa stack + - Worker loop changed (hang situation solved with TTN V3) + - OLED display added + - DEVEUI obtained from BoardID, (same SW for multiple sensors) + - use the TWO processor cores of ESP (one core for audio and one core for LoRa) + - DC offset MEMS compensated by moving average window + - payload compressed from 27 to 19 bytes + Changed 1/11/2023 + - Joining TTN problems solved, join was disturbed by I2S driver, it is stopped during join + - Working loop improved, sleep is postponed after send, and a shorter sleep during joining phase + - TTN SF9 is the default and ADR is enabled + --------------------------------------------------------------------*/ + +#include +#include "lora.h" +#include "soundsensor.h" +#include "measurement.h" +#include "config.h" +#include "oled.h" +static Oled oled; + +// forward declarations +void Task0code( void * pvParameters ); +void loracallback( unsigned int port, unsigned char* msg, unsigned int len); +void loraWorker( ); +static void composeMessage( Measurement& la, Measurement& lc, Measurement& lz); + +static int cycleTime = CYCLETIME; +static char deveui[40]; + +// Weighting lists + static float aweighting[] = A_WEIGHTING; + static float cweighting[] = C_WEIGHTING; + static float zweighting[] = Z_WEIGHTING; + +// measurement buffers, filled by core 0, read by core 1 + static Measurement aMeasurement( aweighting); + static Measurement cMeasurement( cweighting); + static Measurement zMeasurement( zweighting); + +// Task 1 is the default ESP core 1, this one handles the LoRa TTN messages +// Task 0 is the added ESP core 0, this one handles the audio, (read MEMS, FFT process and compose message) +TaskHandle_t Task0; + +// task semaphores +static bool audioRequest = false; +static bool audioReady = false; +static bool sound = false; + +// payloadbuffer +unsigned char payload[80]; +int payloadLength = 0; + +// create soundsensor +static SoundSensor soundSensor; + + void setup() { + Serial.begin(115200); + delay(100); + // LoRa send LED + pinMode(LED_BUILTIN, OUTPUT); + digitalWrite( LED_BUILTIN, LOW); // off + +// get chip id, to be used for DEVEUI LoRa + uint64_t chipid = ESP.getEfuseMac(); //The chip ID is essentially its MAC address(length: 6 bytes). + sprintf( deveui, "%08X%08X", (uint16_t)(chipid >> 32), (uint32_t)chipid); + printf("deveui=%s\n", deveui); + + oled.begin(); + oled.deveui = deveui; + oled.values( &aMeasurement, &cMeasurement, &zMeasurement); + oled.status = "Starting"; + oled.update( ); + + //create a task that will be executed in the Task1code() function, with priority 1 and executed on core 0 + xTaskCreatePinnedToCore( + Task0code, // Task function. + "Task0", // name of task. + 40000, // Stack size of task + NULL, // parameter of the task + 1, // priority of the task 1 + &Task0, // Task handle to keep track of created task + 0); // pin task to core 0 + + +// do testread for 2 seconds + sound = true; + delay(2000); + audioRequest = true; + while( !audioReady) + delay(10); + audioReady = false; + sound = false; + oled.update(); + +//initialize LoRa + loraBegin( APPEUI, deveui, APPKEY); + loraSetRxHandler( loracallback); // set LoRa receive handler (downnlink) + loraSetWorker( loraWorker); // set Worker handler + loraSleep(1); // start worker + Serial.println("end setup"); +} + +// Handle sound measurements +// Note this is anoher theread running in another Core!!!! +void Task0code( void * pvParameters ){ + Serial.print("Task0 running on core "); + Serial.println(xPortGetCoreID()); + soundSensor.begin(); + soundSensor.offset( MIC_OFFSET ); + + // main loop task 0 + while( true){ + + if( sound ) { + if( !soundSensor.running()) + soundSensor.start(); + // read chunk form MEMS and perform FFT, and sum energy in octave bins + float* energy = soundSensor.readSamples(); + + // update + aMeasurement.update( energy); + cMeasurement.update( energy); + zMeasurement.update( energy); + + // calculate audio result on request + if( audioRequest) { + audioRequest = false; + aMeasurement.calculate(); + cMeasurement.calculate(); + zMeasurement.calculate(); + audioReady = true; // signal worker task that audio result is ready + } + } + else { + if( soundSensor.running()) + soundSensor.stop(); + delay(100); // do nothing + } + } +} + +// LoRa receive handler (downnlink) +void loracallback( unsigned int port, unsigned char* msg, unsigned int len) { + printf("lora download message received port=%d len=%d\n", port, len); + + // change cycle count in seconds with a remote TTN download command + // byte 0 is low byte 1 is high byte + if( len >=2) { + int value = msg[0] + 256 * msg[1]; + if( port == 20) { // change cycle time (value must ne between 10 and 600 seconds) + if( value >= 10 && value <= 600) { + cycleTime = value; + printf( "cycleTime changed to %d sec.\n" , value); + } + } + if( port == 21) { // change dB offset (value/10 must be between -40dB and +40dB) + if( value >= -400 && value <= 400) { + float dB = value / 10.0; + soundSensor.offset( dB); + Serial.print( "Mic offset changed to "); Serial.println( dB); + } + } + } +} + +// compose payload message +static void composeMessage( Measurement& la, Measurement& lc, Measurement& lz) { + // find max value to compress values [0 .. max] in an unsigned byte from [0 .. 255] + float max = ( la.max > lc.max) ? la.max : lc.max; + max = ( lz.max > max) ? lz.max : max; + + float c = 255.0 / max; + int i=0; + payload[ i++] = round(max); // save this constant as first byte in the message + + payload[ i++] = round(c * la.min); + payload[ i++] = round(c * la.max); + payload[ i++] = round(c * la.avg); + + payload[ i++] = round(c * lc.min); + payload[ i++] = round(c * lc.max); + payload[ i++] = round(c * lc.avg); + + payload[ i++] = round(c * lz.min); + payload[ i++] = round(c * lz.max); + payload[ i++] = round(c * lz.avg); + + for ( int j = 0; j < OCTAVES; j++) { + payload[ i++] = round(c * lz.spectrum[j]); + } + + payloadLength = i; + if( payloadLength > 51) // max TTN message length + printf( "message to big length=%d\n", payloadLength); +} + +// called from LoRa Task (task1), each cycle time +void loraWorker( ) { + printf("Worker\n"); + + if( loraConnected()) { + sound = true; + oled.status = "TTN Connected"; + audioRequest = true; // signal audiotask to compose an audio report + + while( !audioReady) // wait for Task 0 to be ready + loraLoop(); + audioReady = false; + digitalWrite( LED_BUILTIN, HIGH); + + // save values for oled display + oled.update(); + // debug info, should be comment out + //aMeasurement.print(); + //cMeasurement.print(); + //zMeasurement.print(); + composeMessage( aMeasurement, cMeasurement, zMeasurement); + printf("send message len=%d core=%d\n", payloadLength, xPortGetCoreID()); + loraSend( 22, (unsigned char*)payload, payloadLength); // use port 22 + // wait until lora request is ready within timeout + //long start = millis(); + while ( !loraTxReady() /*&& millis() - start < 100000 */ ) + loraLoop(); + digitalWrite( LED_BUILTIN, LOW); + loraSleep( cycleTime); + } + else { // lora not connected so do a (re)join + sound = false; + digitalWrite( LED_BUILTIN, HIGH); + printf("loraJoin\n"); + oled.status = "TTN Joining.."; + oled.update(); + loraJoin(); + // wait until lora request is ready within timeout + //long start = millis(); + while ( !loraTxReady() /*&& millis() - start < 1000*/ ) + loraLoop(); + digitalWrite( LED_BUILTIN, LOW); + if( loraConnected()) { + oled.status = "TTN Connected"; + sound = true; + oled.update(); + loraSleep( cycleTime); + } + else { + oled.status = "TTN Join Failed"; + oled.update(); + loraSleep( 10); // sleep a short time to retry a join again + } + } +} + +// main loop task 1 (esp default) +void loop() { + loraLoop(); +} diff --git a/src/lorasoundkit/measurement.cpp b/src/measurement.cpp similarity index 74% rename from src/lorasoundkit/measurement.cpp rename to src/measurement.cpp index 02ca988..2875215 100644 --- a/src/lorasoundkit/measurement.cpp +++ b/src/measurement.cpp @@ -33,13 +33,13 @@ Measurement::Measurement( float* weighting) { } void Measurement::reset() { - avg = min = max = 0.0; + _avg = 0.0; _n = 0; - min = FLT_MAX; - max = FLT_MIN; + _min = FLT_MAX; + _max = FLT_MIN; for ( int i = 0; i < OCTAVES; i++) - spectrum[i] = 0.0; + _spectrum[i] = 0.0; } void Measurement::update( float* energies ) { @@ -47,25 +47,27 @@ void Measurement::update( float* energies ) { float sum = 0.0; // sum in energy for this measurement for (int i = 0; i < OCTAVES; i++) { float v = energies[i] * _weighting[i]; - spectrum[i] += v; // sum energy per band for all measurements + _spectrum[i] += v; // sum energy per band for all measurements sum += v; } - avg += sum; + _avg += sum; - if ( max < sum) max = sum; - if ( min > sum) min = sum; + if ( _max < sum) _max = sum; + if ( _min > sum) _min = sum; } void Measurement::calculate() { - avg = decibel( avg / (float)_n); // calculate average and convert to dB - min = decibel( min); // convert to dB - max = decibel( max); // convert to dB + avg = decibel( _avg / (float)_n); // calculate average and convert to dB + min = decibel( _min); // convert to dB + max = decibel( _max); + n = _n; // convert to dB // calculate average for each band and convert to dB for ( int i = 0; i < OCTAVES; i++) { - float val = spectrum[i] / (float)_n; // energy average + float val = _spectrum[i] / (float)_n; // energy average spectrum[i] = decibel( val); // convert to dB } + reset(); } float Measurement::decibel(float v) { @@ -73,7 +75,7 @@ float Measurement::decibel(float v) { } void Measurement::print() { - printf("count=%d min=%.1f max=%.1f avg=%.1f =>", _n, min, max, avg); + printf("count=%d min=%.1f max=%.1f avg=%.1f =>", n, min, max, avg); for (int i = 0; i < OCTAVES; i++) printf(" %.1f", spectrum[i]); printf("\n"); diff --git a/src/lorasoundkit/measurement.h b/src/measurement.h similarity index 86% rename from src/lorasoundkit/measurement.h rename to src/measurement.h index 99c9dbf..1866325 100644 --- a/src/lorasoundkit/measurement.h +++ b/src/measurement.h @@ -68,7 +68,7 @@ class Measurement { /// \param [in] energies ? void update( float* energies); - /// \param Calculate what? + /// \param calculate result void calculate(); /// \brief calculate dB value for power @@ -80,15 +80,13 @@ class Measurement { void print(); // public members - /// \todo remove public members and add functions to handle member variables - /// to meet principle of data-hiding. - /// \todo rename member variables with prefix '_' to indicate member variables. - float spectrum[OCTAVES]; ///< Array of results in dB per frequency band. - float min, max; ///< min and max value in dB. - float avg; ///< All average in dB based on energy. + float avg, min, max; ///< avg, min and max result value in dB. + int n; private: + float _spectrum[OCTAVES]; ///< working array in energy per frequency band. + float _avg, _min, _max; ///< working avg, min , max based in energy. float* _weighting; ///< Weighting factors int _n; ///< number of measurements }; diff --git a/src/oled.cpp b/src/oled.cpp new file mode 100644 index 0000000..2008ffa --- /dev/null +++ b/src/oled.cpp @@ -0,0 +1,58 @@ +/******************************************************************************* +* file oled.h +* Oled wrapper, interface to display messages +* author Marcel Meek +*********************************************************************************/ + +#include "oled.h" +#define VERSION "4.0" + +//OLED pins +#define OLED_SDA 21 // v1=4, v2=21 +#define OLED_SCL 22 // v1=15, v2=22 +//#define OLED_RST -1 // v1=16, v2=-1 +#define SCREEN_WIDTH 128 // OLED display width, in pixels +#define SCREEN_HEIGHT 64 // OLED display height, in pixels + + +Oled::Oled() { + display = new Adafruit_SSD1306( SCREEN_WIDTH, SCREEN_HEIGHT, &Wire); //OLED_RST); +}; + +Oled::~Oled() { + delete display; +} + +void Oled::begin() { +//initialize OLED + Wire.begin(OLED_SDA, OLED_SCL); + if (!display->begin(SSD1306_SWITCHCAPVCC, 0x3c, false, false)) { // Address 0x3C for 128x32 + printf("SSD1306 allocation failed\n"); + for (;;); // Don't proceed, loop forever + } +} + +void Oled::values( Measurement* la, Measurement* lc, Measurement* lz) { + _la = la; + _lc = lc; + _lz = lz; +} + + +void Oled::update( ) { + //printf( "showValues status=%s\n", status); + display->clearDisplay(); + //display->setRotation( 2); // rotate 180 degrees + display->setTextColor(WHITE); + display->setTextSize(1); + if( _la != NULL && _la->avg > 0.0) { + display->setCursor(0, 0); display->printf( " avg min max"); + display->setCursor(0, 10); display->printf("dB(A) %.1f %.1f %.1f", _la->avg, _la->min, _la->max), + display->setCursor(0, 20); display->printf("dB(C) %.1f %.1f %.1f", _lc->avg, _lc->min, _lc->max); + display->setCursor(0, 30); display->printf("dB(Z) %.1f %.1f %.1f", _lz->avg, _lz->min, _lz->max); + } + display->setCursor(0, 40); display->print( deveui); + display->setCursor(0, 50); display->print( status); + display->display(); +} + diff --git a/src/lorasoundkit/oled.h b/src/oled.h similarity index 59% rename from src/lorasoundkit/oled.h rename to src/oled.h index 4e3f5d3..5ca5e44 100644 --- a/src/lorasoundkit/oled.h +++ b/src/oled.h @@ -10,16 +10,23 @@ #include #include #include +#include +#include "measurement.h" class Oled { public: Oled(); ~Oled(); - void begin(); - void showValues( float la, float lc, float lz, bool ttnOk); + void begin(); + void values( Measurement* la, Measurement* lc, Measurement* lz); + void update(); + + const char* status; + const char* deveui; private: - Adafruit_SSD1306 *display; + Measurement *_la, *_lc, *_lz; + Adafruit_SSD1306 *display; }; #endif //__MEASUREMENT_H_ diff --git a/src/lorasoundkit/soundsensor.cpp b/src/soundsensor.cpp similarity index 74% rename from src/lorasoundkit/soundsensor.cpp rename to src/soundsensor.cpp index e679b6e..e471de8 100644 --- a/src/lorasoundkit/soundsensor.cpp +++ b/src/soundsensor.cpp @@ -1,172 +1,217 @@ -/*-------------------------------------------------------------------- - This file is part of the TTN-Apeldoorn Sound Sensor. - - This code is free software: - you can redistribute it and/or modify it under the terms of a Creative - Commons Attribution-NonCommercial 4.0 International License - (http://creativecommons.org/licenses/by-nc/4.0/) by - TTN-Apeldoorn (https://www.thethingsnetwork.org/community/apeldoorn/) - - The program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - --------------------------------------------------------------------*/ - -/*! - * \file SoundSensor.cpp - * \author Marcel Meek, Remko Welling (remko@rfsee.nl) - */ - -#include "soundsensor.h" -#include "arduinoFFT.h" -#include "config.h" - -const i2s_port_t I2S_PORT = I2S_NUM_0; - -// The I2S config as per the example -const i2s_config_t i2s_config = { - .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), // Receive, not transfer - .sample_rate = SAMPLE_FREQ, - .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, // only 24 bits are used - .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // although the SEL config should be left, it seems to transmit on right, or not - .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), - .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // Interrupt level 1 - .dma_buf_count = 8, // number of buffers - .dma_buf_len = 1024, //BLOCK_SIZE, samples per buffer - .use_apll = true -}; - -// pin config MEMS microphone -#if defined(ARDUINO_TTGO_LoRa32_V1) -// define IO pins TTGO LoRa32 V1 for I2S for MEMS microphone -const i2s_pin_config_t pin_config = { - .bck_io_num = 13, - .ws_io_num = 12, - .data_out_num = -1, - .data_in_num = 35 //changed to 35, (21 is used by i2c) -}; -#elif defined(ARDUINO_ESP32_DEV) -// define IO pins Sparkfun for I2S for MEMS microphone -const i2s_pin_config_t pin_config = { - .bck_io_num = 18, - .ws_io_num = 23, - .data_out_num = -1, - .data_in_num = 19 -}; -#else - #error Unsupported board selection. -#endif - -SoundSensor::SoundSensor() { - _fft = new arduinoFFT(_real, _imag, SAMPLES, SAMPLES); - _offset = 0; -} - -SoundSensor::~SoundSensor(){ - delete _fft; -} - -void SoundSensor::begin(){ - - // Configuring the I2S driver and pins. - // This function must be called before any I2S driver read/write operations. - _err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL); - if (_err != ESP_OK) { - printf("Failed installing I2S driver: %d\n", _err); - while (true); - } - - _err = i2s_set_pin(I2S_PORT, &pin_config); - if (_err != ESP_OK) { - printf("Failed setting pin: %d\n", _err); - while (true); - } - printf("I2S driver installed.\n"); -} - -float* SoundSensor::readSamples(){ - // Read multiple samples at once and calculate the sound pressure - size_t num_bytes_read; - _err = i2s_read( - I2S_PORT, - (char *) _samples, - BLOCK_SIZE * 4, // 4 bytes per sample - &num_bytes_read, - portMAX_DELAY - ); // no timeout - - // printf("bytes read %d\n", num_bytes_read); - - if(_err != ESP_OK){ - printf("%d err\n",_err); - } - - integerToFloat(_samples, _real, _imag, SAMPLES); - - // apply HANN window, optimal for energy calculations - _fft->Windowing(FFT_WIN_TYP_HANN, FFT_FORWARD); // changed was FFT_WIN_TYP_FLT_TOP - - // do FFT processing - _fft->Compute(FFT_FORWARD); - - // calculate energy in each bin - calculateEnergy(_real, _imag, SAMPLES); - - // sum up energy in bin for each octave - sumEnergy(_real, _energy); - - return _energy; -} - -// convert WAV integers to float -// convert 24 High bits from I2S buffer to float and divide * 256 -// remove DC offset, necessary for some MEMS microphones -void SoundSensor::integerToFloat(int32_t * samples, float *vReal, float *vImag, uint16_t size) { - float sum = 0.0; - for (uint16_t i = 0; i < size; i++) { - int32_t val = (samples[i] >> 8); // move 24 value bits on the correct place in a long - sum += (float)val; - samples[i] = (val - _offset ) << 8; // DC component removed, and move back to original buffer - vReal[i] = (float)val / (256.0 * FACTOR); // adjustment - vImag[i] = 0.0; - } - _offset = sum / size; //dc component - //printf("DC offset %d\n", offset); -} - -// calculates energy from Re and Im parts and places it back in the Re part (Im part is zeroed) -void SoundSensor::calculateEnergy(float *vReal, float *vImag, uint16_t samples) -{ - for (uint16_t i = 0; i < samples; i++) { - vReal[i] = sq(vReal[i]) + sq(vImag[i]); - vImag[i] = 0.0; - } -} - -// sums up energy in whole octave bins -void SoundSensor::sumEnergy(const float *samples, float *energies) { - - // skip the first two bins - int bin_size = 2; - int bin = bin_size; - for (int octave = 0; octave < OCTAVES; octave++){ - float sum = 0.0; - for (int i = 0; i < bin_size; i++){ - sum += samples[bin++]; - } - energies[octave] = sum; - bin_size *= 2; - //printf("octaaf=%d, bin=%d, sum=%f\n", octave, bin-1, sum); - } -} - -/* -// generate test sinus -static void generateSineWave( int32_t* samples, float amplitude, float freq) { - float c = round( freq / (SAMPLE_FREQ / (float)SAMPLES)) / SAMPLES; // put a multipe of complete sinewaves in buffer - for ( int i = 0; i < BLOCK_SIZE; i++) { - int32_t temp = 256 * amplitude * sin((float)i * c * twoPi ); // sine wave - samples[i] = temp & 0xFFFFFF00; // convert to WAV integers - } -} -*/ +/*-------------------------------------------------------------------- + This file is part of the TTN-Apeldoorn Sound Sensor. + + This code is free software: + you can redistribute it and/or modify it under the terms of a Creative + Commons Attribution-NonCommercial 4.0 International License + (http://creativecommons.org/licenses/by-nc/4.0/) by + TTN-Apeldoorn (https://www.thethingsnetwork.org/community/apeldoorn/) + + The program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + --------------------------------------------------------------------*/ + +/*! + * \file SoundSensor.cpp + * \author Marcel Meek, Remko Welling (remko@rfsee.nl) + */ + +#include "soundsensor.h" +#include "arduinoFFT.h" + +const i2s_port_t I2S_PORT = I2S_NUM_0; + +// The I2S config as per the example +const i2s_config_t i2s_config = { + .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), // Receive, not transfer + .sample_rate = SAMPLE_FREQ, + .bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT, // only 24 bits are used + .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // For ARduino V2.2 changed From LEFT to RIGHT !!!!!! + .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), + .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, // Interrupt level 1 + .dma_buf_count = 8, // number of buffers + .dma_buf_len = 1024, //BLOCK_SIZE, samples per buffer + .use_apll = true +}; + +// pin config MEMS microphone +#define TTGO_LORA32_V21 +#if defined(TTGO_LORA32_V21) +// define IO pins TTGO LoRa32 V1 for I2S for MEMS microphone +const i2s_pin_config_t pin_config = { + .bck_io_num = 00, + .ws_io_num = 12, + .data_out_num = -1, + .data_in_num = 35 +}; +#elif defined(ARDUINO_ESP32_DEV) +// define IO pins Sparkfun for I2S for MEMS microphone +const i2s_pin_config_t pin_config = { + .bck_io_num = 18, + .ws_io_num = 23, + .data_out_num = I2S_PIN_NO_CHANGE, // -1, + .data_in_num = 19 +}; +#else + #error Unsupported board selection. +#endif + +SoundSensor::SoundSensor() { + _fft = new arduinoFFT(_real, _imag, SAMPLES, SAMPLES); + _runningDC = 0.0; + _runningN = 0; + offset( 0.0); + _i2s = false; +} + +SoundSensor::~SoundSensor(){ + i2s_driver_uninstall( I2S_PORT); + delete _fft; +} + +void SoundSensor::begin(){ + + // Configuring the I2S driver and pins. + // This function must be called before any I2S driver read/write operations. + _err = i2s_driver_install(I2S_PORT, &i2s_config, 0, NULL); + // printf("_err=%d\n", _err); +// printf("%d, %d, %d, %d\n", pin_config.bck_io_num, pin_config.ws_io_num, pin_config.data_out_num, pin_config.data_in_num); + if (_err != ESP_OK) { + printf("Failed installing I2S driver: %d\n", _err); + while (true); + } + + _err = i2s_set_pin(I2S_PORT, &pin_config); + if (_err != ESP_OK) { + printf("Failed setting pin: %d\n", _err); + while (true); + } + printf("I2S driver installed.\n"); + stop(); +} + +void SoundSensor::start() { + printf("i2s_start\n"); + i2s_start( I2S_PORT); + _i2s = true; +} + +void SoundSensor::stop() { + printf("i2s_stop\n"); + _i2s = false; + i2s_stop( I2S_PORT); + +} + +float* SoundSensor::readSamples(){ + // Read multiple samples at once and calculate the sound pressure + + size_t num_bytes_read; + _err = i2s_read( + I2S_PORT, + (char *) _samples, + BLOCK_SIZE * 4, // 4 bytes per sample + &num_bytes_read, + portMAX_DELAY + ); // no timeout + + if(_err != ESP_OK){ + printf("%d err\n",_err); + } + + integerToFloat(_samples, _real, _imag, SAMPLES); + + // apply HANN window, optimal for energy calculations + _fft->Windowing(FFT_WIN_TYP_HANN, FFT_FORWARD); // changed was FFT_WIN_TYP_FLT_TOP + + // do FFT processing + _fft->Compute(FFT_FORWARD); + + // calculate energy in each bin + calculateEnergy(_real, _imag, SAMPLES); + + // sum up energy in bin for each octave + sumEnergy(_real, _energy); + + return _energy; +} + +// convert WAV integers to float +// convert 24 High bits from I2S buffer to float and divide * 256 +// remove DC offset, necessary for some MEMS microphones +/*void SoundSensor::integerToFloat(int32_t * samples, float *vReal, float *vImag, uint16_t size) { + float sum = 0.0; + for (uint16_t i = 0; i < size; i++) { + int32_t val = (samples[i] >> 8); // move 24 value bits on the correct place in a long + sum += (float)val; + samples[i] = (val - _offset ) << 8; // DC component removed, and move back to original buffer + vReal[i] = (float)val / (256.0 * FACTOR); // adjustment + vImag[i] = 0.0; + } + _offset = sum / size; //dc component + //printf("DC offset %d\n", offset); +}*/ + +void SoundSensor::integerToFloat(int32_t * samples, float *vReal, float *vImag, uint16_t size) { + float sum = 0.0; + // calculate offset + for (uint16_t i = 0; i < size; i++) { + int32_t val = (samples[i] >> 8); // move 24 value bits on the correct place in a long + vReal[i] = (float)val; + sum += vReal[i]; + } + float offs = sum / (float)size; //dc component + if( _runningN < 100) + _runningN++; + float newDC = _runningDC + (offs - _runningDC)/_runningN; + _runningDC = newDC; + + for (uint16_t i = 0; i < size; i++) { + vReal[i] = (vReal[i] - newDC) / (256.0 * FACTOR / _factor); // 30.0 adjustment + vImag[i] = 0.0; + } + //printf("DC offset %f\n", newDC); +} + +// calculates energy from Re and Im parts and places it back in the Re part (Im part is zeroed) +void SoundSensor::calculateEnergy(float *vReal, float *vImag, uint16_t samples) +{ + for (uint16_t i = 0; i < samples; i++) { + vReal[i] = sq(vReal[i]) + sq(vImag[i]); + vImag[i] = 0.0; + } +} + +// convert dB offset to factor +void SoundSensor::offset( float dB) { + _factor = pow(10, dB / 20.0); // convert dB to factor +} + +// sums up energy in whole octave bins +void SoundSensor::sumEnergy(const float *samples, float *energies) { + + // skip the first two bins + int bin_size = 2; + int bin = bin_size; + for (int octave = 0; octave < OCTAVES; octave++){ + float sum = 0.0; + for (int i = 0; i < bin_size; i++){ + sum += samples[bin++]; + } + energies[octave] = sum; + bin_size *= 2; + //printf("octaaf=%d, bin=%d, sum=%f\n", octave, bin-1, sum); + } +} + +/* +// generate test sinus +static void generateSineWave( int32_t* samples, float amplitude, float freq) { + float c = round( freq / (SAMPLE_FREQ / (float)SAMPLES)) / SAMPLES; // put a multipe of complete sinewaves in buffer + for ( int i = 0; i < BLOCK_SIZE; i++) { + int32_t temp = 256 * amplitude * sin((float)i * c * twoPi ); // sine wave + samples[i] = temp & 0xFFFFFF00; // convert to WAV integers + } +} +*/ diff --git a/src/lorasoundkit/soundsensor.h b/src/soundsensor.h similarity index 81% rename from src/lorasoundkit/soundsensor.h rename to src/soundsensor.h index 011ed44..00f8a90 100644 --- a/src/lorasoundkit/soundsensor.h +++ b/src/soundsensor.h @@ -1,73 +1,79 @@ -/*-------------------------------------------------------------------- - This file is part of the TTN-Apeldoorn Sound Sensor. - - This code is free software: - you can redistribute it and/or modify it under the terms of a Creative - Commons Attribution-NonCommercial 4.0 International License - (http://creativecommons.org/licenses/by-nc/4.0/) by - TTN-Apeldoorn (https://www.thethingsnetwork.org/community/apeldoorn/) - - The program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - --------------------------------------------------------------------*/ - -/*! - * \file SoundSensor.cpp - * \author Marcel Meek, Remko Welling (remko@rfsee.nl) - */ - -#ifndef __SOUND_SENSOR_H_ -#define __SOUND_SENSOR_H_ - -#include -#include -#include "arduinoFFT.h" - -#define FACTOR 30.0 /// \todo to be cheked why this 10.0 ? - -// size of noise sample -#define SAMPLES 2048 //1024 ///< at sample frequency of 22,627 kHz with 2048 samples, duration is 90 ms. -#define SAMPLE_FREQ 22627 ///< this makes a bin bandwith of 22627 / 2048 = 11 Hz -#define OCTAVES 9 - -const int BLOCK_SIZE = SAMPLES; - -class SoundSensor { - public: - - /// \brief constructor - SoundSensor(); - ~SoundSensor(); - - /// \brief Initialize Sound sensor class and start. - void begin(); - - // Read multiple samples at once and calculate the sound pressure - // returns energy in octave bands - float* readSamples(); - - private: - arduinoFFT *_fft; ///< FFT class - // FFT buffers - float _real[SAMPLES]; - float _imag[SAMPLES]; - float _energy[OCTAVES]; - int32_t _samples[BLOCK_SIZE]; - int32_t _offset; ///< variable to compensate for DC-offset - - esp_err_t _err; ///< Variable to store errors from ESP32 - - /// \brief Convert integer to float - void integerToFloat(int32_t *samples, float *vReal, float *vImag, uint16_t size); - - // calculates energy from Re and Im parts and places it back in the Re part (Im part is zeroed) - void calculateEnergy(float *vReal, float *vImag, uint16_t samples); - - // sums up energy in whole octave bins - void sumEnergy(const float *samples, float *energies); - // sums up energy in terts bins - void sumEnergy3(const float *samples, float *energies); -}; - -#endif // __SOUND_SENSOR_H_ +/*-------------------------------------------------------------------- + This file is part of the TTN-Apeldoorn Sound Sensor. + + This code is free software: + you can redistribute it and/or modify it under the terms of a Creative + Commons Attribution-NonCommercial 4.0 International License + (http://creativecommons.org/licenses/by-nc/4.0/) by + TTN-Apeldoorn (https://www.thethingsnetwork.org/community/apeldoorn/) + + The program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + --------------------------------------------------------------------*/ + +/*! + * \file SoundSensor.cpp + * \author Marcel Meek, Remko Welling (remko@rfsee.nl) + */ + +#ifndef __SOUND_SENSOR_H_ +#define __SOUND_SENSOR_H_ + +#include +#include +#include "arduinoFFT.h" + +#define FACTOR 30.0 /// \todo to be cheked why this 10.0 ? + +// size of noise sample +#define SAMPLES 2048 //1024 ///< at sample frequency of 22,627 kHz with 2048 samples, duration is 90 ms. +#define SAMPLE_FREQ 22627 ///< this makes a bin bandwith of 22627 / 2048 = 11 Hz +#define OCTAVES 9 + +const int BLOCK_SIZE = SAMPLES; + +class SoundSensor { + public: + + /// \brief constructor + SoundSensor(); + ~SoundSensor(); + + /// \brief Initialize Sound sensor class and start. + void begin(); + void start(); + void stop(); + bool running() { return _i2s; } + + // Read multiple samples at once and calculate the sound pressure + // returns energy in octave bands + float* readSamples(); + void offset( float dB); ///< mic. correction in dB + + private: + arduinoFFT *_fft; ///< FFT class + // FFT buffers + float _real[SAMPLES]; + float _imag[SAMPLES]; + float _energy[OCTAVES]; + int32_t _samples[BLOCK_SIZE]; + float _runningDC = 0.0; // compensate MEMS DC offset + int _runningN = 0; // running DSC offset average count + float _factor; ///< mic. correction factor + esp_err_t _err; ///< Variable to store errors from ESP32 + boolean _i2s; + + /// \brief Convert integer to float + void integerToFloat(int32_t *samples, float *vReal, float *vImag, uint16_t size); + + // calculates energy from Re and Im parts and places it back in the Re part (Im part is zeroed) + void calculateEnergy(float *vReal, float *vImag, uint16_t samples); + + // sums up energy in whole octave bins + void sumEnergy(const float *samples, float *energies); + // sums up energy in terts bins + void sumEnergy3(const float *samples, float *energies); +}; + +#endif // __SOUND_SENSOR_H_ diff --git a/test/Test-report-TTGO-LoRa-Soundkit.pdf b/test/Test-report-TTGO-LoRa-Soundkit.pdf new file mode 100644 index 0000000..b5d4335 Binary files /dev/null and b/test/Test-report-TTGO-LoRa-Soundkit.pdf differ diff --git a/ttn-payload/TTN-V3-payloadformatter.js b/ttn-payload/TTN-V3-payloadformatter.js new file mode 100644 index 0000000..cfc2f61 --- /dev/null +++ b/ttn-payload/TTN-V3-payloadformatter.js @@ -0,0 +1,59 @@ +function decodeUplink(input) { + + // Payload decoder for Lora Soundkit. + // Decode binary uplink message to json message + // the payload byte buffer contains 19 bytes in the order: + // byte 0: a constant (this constant is a multiply factor to correct byte values 1 upto 18) + // byte 1-9: 9 bytes containg la.min, la.max, la.avg, lc.min, lc.max, lc.avg, lz.min, lz.max, lz.avg + // byte 10-18: 9 bytes containing lz spectrum representing octaves from 31.5Hz to 8kHz + // the payload formatter calculates from the lz spectrum the lc and la spectrum + // the constant in byte 0 corrects the values in byte 1 upto 18 + // by Marcel Meek, May 2020 + + // weigthing tables + var aWeighting = [ -39.4, -26.2, -16.1, -8.6, -3.2, 0.0, 1.2, 1.0, -1.1 ]; + var cWeighting = [ -3.0, -0.8, -0.2, 0.0, 0.0, 0.0, 0.2, 0.3, -3.0 ]; + var len = aWeighting.length; + + var decoded = {}; // json result + var bytes = input.bytes; + var i = 0; + + // decode 19 bytes payload (new format) + if (input.fPort === 22) { + var max = bytes[i++]; + var c = max / 255.0; + + decoded.la = {}; + decoded.lc = {}; + decoded.lz = {}; + + // get min, max and avg for LA, LC and LZ + decoded.la.min = c * bytes[i++]; + decoded.la.max = c * bytes[i++]; + decoded.la.avg = c * bytes[i++]; + decoded.lc.min = c * bytes[i++]; + decoded.lc.max = c * bytes[i++]; + decoded.lc.avg = c * bytes[i++]; + decoded.lz.min = c * bytes[i++]; + decoded.lz.max = c * bytes[i++]; + decoded.lz.avg = c * bytes[i++]; + + // get LZ spectrum + decoded.lz.spectrum = []; + for( j=0; j