diff --git a/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/include/README b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/include/README new file mode 100644 index 00000000..194dcd43 --- /dev/null +++ b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/lib/README b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/lib/README new file mode 100644 index 00000000..6debab1e --- /dev/null +++ b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/platformio.ini b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/platformio.ini new file mode 100644 index 00000000..8836ab42 --- /dev/null +++ b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/platformio.ini @@ -0,0 +1,23 @@ +; 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:seeed_wio_terminal] +platform = atmelsam +board = seeed_wio_terminal +framework = arduino +lib_deps = + seeed-studio/Seeed Arduino FS @ 2.1.1 + seeed-studio/Seeed Arduino SFUD @ 2.0.2 + seeed-studio/Seeed Arduino rpcWiFi @ 1.0.5 + seeed-studio/Seeed Arduino rpcUnified @ 2.1.3 + seeed-studio/Seeed_Arduino_mbedtls @ 3.0.1 + seeed-studio/Seeed Arduino RTC @ 2.0.0 + bblanchon/ArduinoJson @ 6.17.3 + contrem/arduino-timer @ 2.3.0 diff --git a/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/config.h b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/config.h new file mode 100644 index 00000000..f2e912c4 --- /dev/null +++ b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/config.h @@ -0,0 +1,93 @@ +#pragma once + +#define RATE 16000 +#define SAMPLE_LENGTH_SECONDS 4 +#define SAMPLES RATE * SAMPLE_LENGTH_SECONDS +#define BUFFER_SIZE (SAMPLES * 2) + 44 +#define ADC_BUF_LEN 1600 + +const char *SSID = ""; +const char *PASSWORD = ""; + +const char *SPEECH_API_KEY = ""; +const char *SPEECH_LOCATION = ""; +const char *LANGUAGE = ""; + +const char *TOKEN_URL = "https://%s.api.cognitive.microsoft.com/sts/v1.0/issuetoken"; +const char *SPEECH_URL = "https://%s.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1?language=%s"; + +const char *TEXT_TO_TIMER_FUNCTION_URL = "http://:7071/api/text-to-timer"; +const char *GET_VOICES_FUNCTION_URL = "http://:7071/api/get-voices"; +const char *TEXT_TO_SPEECH_FUNCTION_URL = "http://:7071/api/text-to-speech"; + +const char *TOKEN_CERTIFICATE = + "-----BEGIN CERTIFICATE-----\r\n" + "MIIF8zCCBNugAwIBAgIQAueRcfuAIek/4tmDg0xQwDANBgkqhkiG9w0BAQwFADBh\r\n" + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\n" + "d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\r\n" + "MjAeFw0yMDA3MjkxMjMwMDBaFw0yNDA2MjcyMzU5NTlaMFkxCzAJBgNVBAYTAlVT\r\n" + "MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKjAoBgNVBAMTIU1pY3Jv\r\n" + "c29mdCBBenVyZSBUTFMgSXNzdWluZyBDQSAwNjCCAiIwDQYJKoZIhvcNAQEBBQAD\r\n" + "ggIPADCCAgoCggIBALVGARl56bx3KBUSGuPc4H5uoNFkFH4e7pvTCxRi4j/+z+Xb\r\n" + "wjEz+5CipDOqjx9/jWjskL5dk7PaQkzItidsAAnDCW1leZBOIi68Lff1bjTeZgMY\r\n" + "iwdRd3Y39b/lcGpiuP2d23W95YHkMMT8IlWosYIX0f4kYb62rphyfnAjYb/4Od99\r\n" + "ThnhlAxGtfvSbXcBVIKCYfZgqRvV+5lReUnd1aNjRYVzPOoifgSx2fRyy1+pO1Uz\r\n" + "aMMNnIOE71bVYW0A1hr19w7kOb0KkJXoALTDDj1ukUEDqQuBfBxReL5mXiu1O7WG\r\n" + "0vltg0VZ/SZzctBsdBlx1BkmWYBW261KZgBivrql5ELTKKd8qgtHcLQA5fl6JB0Q\r\n" + "gs5XDaWehN86Gps5JW8ArjGtjcWAIP+X8CQaWfaCnuRm6Bk/03PQWhgdi84qwA0s\r\n" + "sRfFJwHUPTNSnE8EiGVk2frt0u8PG1pwSQsFuNJfcYIHEv1vOzP7uEOuDydsmCjh\r\n" + "lxuoK2n5/2aVR3BMTu+p4+gl8alXoBycyLmj3J/PUgqD8SL5fTCUegGsdia/Sa60\r\n" + "N2oV7vQ17wjMN+LXa2rjj/b4ZlZgXVojDmAjDwIRdDUujQu0RVsJqFLMzSIHpp2C\r\n" + "Zp7mIoLrySay2YYBu7SiNwL95X6He2kS8eefBBHjzwW/9FxGqry57i71c2cDAgMB\r\n" + "AAGjggGtMIIBqTAdBgNVHQ4EFgQU1cFnOsKjnfR3UltZEjgp5lVou6UwHwYDVR0j\r\n" + "BBgwFoAUTiJUIBiV5uNu5g/6+rkS7QYXjzkwDgYDVR0PAQH/BAQDAgGGMB0GA1Ud\r\n" + "JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMHYG\r\n" + "CCsGAQUFBwEBBGowaDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu\r\n" + "Y29tMEAGCCsGAQUFBzAChjRodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln\r\n" + "aUNlcnRHbG9iYWxSb290RzIuY3J0MHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6Ly9j\r\n" + "cmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5jcmwwN6A1oDOG\r\n" + "MWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5j\r\n" + "cmwwHQYDVR0gBBYwFDAIBgZngQwBAgEwCAYGZ4EMAQICMBAGCSsGAQQBgjcVAQQD\r\n" + "AgEAMA0GCSqGSIb3DQEBDAUAA4IBAQB2oWc93fB8esci/8esixj++N22meiGDjgF\r\n" + "+rA2LUK5IOQOgcUSTGKSqF9lYfAxPjrqPjDCUPHCURv+26ad5P/BYtXtbmtxJWu+\r\n" + "cS5BhMDPPeG3oPZwXRHBJFAkY4O4AF7RIAAUW6EzDflUoDHKv83zOiPfYGcpHc9s\r\n" + "kxAInCedk7QSgXvMARjjOqdakor21DTmNIUotxo8kHv5hwRlGhBJwps6fEVi1Bt0\r\n" + "trpM/3wYxlr473WSPUFZPgP1j519kLpWOJ8z09wxay+Br29irPcBYv0GMXlHqThy\r\n" + "8y4m/HyTQeI2IMvMrQnwqPpY+rLIXyviI2vLoI+4xKE4Rn38ZZ8m\r\n" + "-----END CERTIFICATE-----\r\n"; + +const char *SPEECH_CERTIFICATE = + "-----BEGIN CERTIFICATE-----\r\n" + "MIIF8zCCBNugAwIBAgIQCq+mxcpjxFFB6jvh98dTFzANBgkqhkiG9w0BAQwFADBh\r\n" + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\n" + "d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\r\n" + "MjAeFw0yMDA3MjkxMjMwMDBaFw0yNDA2MjcyMzU5NTlaMFkxCzAJBgNVBAYTAlVT\r\n" + "MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKjAoBgNVBAMTIU1pY3Jv\r\n" + "c29mdCBBenVyZSBUTFMgSXNzdWluZyBDQSAwMTCCAiIwDQYJKoZIhvcNAQEBBQAD\r\n" + "ggIPADCCAgoCggIBAMedcDrkXufP7pxVm1FHLDNA9IjwHaMoaY8arqqZ4Gff4xyr\r\n" + "RygnavXL7g12MPAx8Q6Dd9hfBzrfWxkF0Br2wIvlvkzW01naNVSkHp+OS3hL3W6n\r\n" + "l/jYvZnVeJXjtsKYcXIf/6WtspcF5awlQ9LZJcjwaH7KoZuK+THpXCMtzD8XNVdm\r\n" + "GW/JI0C/7U/E7evXn9XDio8SYkGSM63aLO5BtLCv092+1d4GGBSQYolRq+7Pd1kR\r\n" + "EkWBPm0ywZ2Vb8GIS5DLrjelEkBnKCyy3B0yQud9dpVsiUeE7F5sY8Me96WVxQcb\r\n" + "OyYdEY/j/9UpDlOG+vA+YgOvBhkKEjiqygVpP8EZoMMijephzg43b5Qi9r5UrvYo\r\n" + "o19oR/8pf4HJNDPF0/FJwFVMW8PmCBLGstin3NE1+NeWTkGt0TzpHjgKyfaDP2tO\r\n" + "4bCk1G7pP2kDFT7SYfc8xbgCkFQ2UCEXsaH/f5YmpLn4YPiNFCeeIida7xnfTvc4\r\n" + "7IxyVccHHq1FzGygOqemrxEETKh8hvDR6eBdrBwmCHVgZrnAqnn93JtGyPLi6+cj\r\n" + "WGVGtMZHwzVvX1HvSFG771sskcEjJxiQNQDQRWHEh3NxvNb7kFlAXnVdRkkvhjpR\r\n" + "GchFhTAzqmwltdWhWDEyCMKC2x/mSZvZtlZGY+g37Y72qHzidwtyW7rBetZJAgMB\r\n" + "AAGjggGtMIIBqTAdBgNVHQ4EFgQUDyBd16FXlduSzyvQx8J3BM5ygHYwHwYDVR0j\r\n" + "BBgwFoAUTiJUIBiV5uNu5g/6+rkS7QYXjzkwDgYDVR0PAQH/BAQDAgGGMB0GA1Ud\r\n" + "JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMHYG\r\n" + "CCsGAQUFBwEBBGowaDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu\r\n" + "Y29tMEAGCCsGAQUFBzAChjRodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln\r\n" + "aUNlcnRHbG9iYWxSb290RzIuY3J0MHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6Ly9j\r\n" + "cmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5jcmwwN6A1oDOG\r\n" + "MWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5j\r\n" + "cmwwHQYDVR0gBBYwFDAIBgZngQwBAgEwCAYGZ4EMAQICMBAGCSsGAQQBgjcVAQQD\r\n" + "AgEAMA0GCSqGSIb3DQEBDAUAA4IBAQAlFvNh7QgXVLAZSsNR2XRmIn9iS8OHFCBA\r\n" + "WxKJoi8YYQafpMTkMqeuzoL3HWb1pYEipsDkhiMnrpfeYZEA7Lz7yqEEtfgHcEBs\r\n" + "K9KcStQGGZRfmWU07hPXHnFz+5gTXqzCE2PBMlRgVUYJiA25mJPXfB00gDvGhtYa\r\n" + "+mENwM9Bq1B9YYLyLjRtUz8cyGsdyTIG/bBM/Q9jcV8JGqMU/UjAdh1pFyTnnHEl\r\n" + "Y59Npi7F87ZqYYJEHJM2LGD+le8VsHjgeWX2CJQko7klXvcizuZvUEDTjHaQcs2J\r\n" + "+kPgfyMIOY1DMJ21NxOJ2xPRC/wAh/hzSBRVtoAnyuxtkZ4VjIOh\r\n" + "-----END CERTIFICATE-----\r\n"; diff --git a/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/flash_stream.h b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/flash_stream.h new file mode 100644 index 00000000..b841f1d0 --- /dev/null +++ b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/flash_stream.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include + +#include "config.h" + +class FlashStream : public Stream +{ +public: + FlashStream() + { + _pos = 0; + _flash_address = 0; + _flash = sfud_get_device_table() + 0; + + populateBuffer(); + } + + virtual size_t write(uint8_t val) + { + return 0; + } + + virtual int available() + { + int remaining = BUFFER_SIZE - ((_flash_address - HTTP_TCP_BUFFER_SIZE) + _pos); + int bytes_available = min(HTTP_TCP_BUFFER_SIZE, remaining); + + if (bytes_available == 0) + { + bytes_available = -1; + } + + return bytes_available; + } + + virtual int read() + { + int retVal = _buffer[_pos++]; + + if (_pos == HTTP_TCP_BUFFER_SIZE) + { + populateBuffer(); + } + + return retVal; + } + + virtual int peek() + { + return _buffer[_pos]; + } + +private: + void populateBuffer() + { + sfud_read(_flash, _flash_address, HTTP_TCP_BUFFER_SIZE, _buffer); + _flash_address += HTTP_TCP_BUFFER_SIZE; + _pos = 0; + } + + size_t _pos; + size_t _flash_address; + const sfud_flash *_flash; + + byte _buffer[HTTP_TCP_BUFFER_SIZE]; +}; diff --git a/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/flash_writer.h b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/flash_writer.h new file mode 100644 index 00000000..87fdff29 --- /dev/null +++ b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/flash_writer.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +class FlashWriter +{ +public: + void init() + { + _flash = sfud_get_device_table() + 0; + _sfudBufferSize = _flash->chip.erase_gran; + _sfudBuffer = new byte[_sfudBufferSize]; + _sfudBufferPos = 0; + _sfudBufferWritePos = 0; + } + + void reset() + { + _sfudBufferPos = 0; + _sfudBufferWritePos = 0; + } + + void writeSfudBuffer(byte b) + { + _sfudBuffer[_sfudBufferPos++] = b; + if (_sfudBufferPos == _sfudBufferSize) + { + sfud_erase_write(_flash, _sfudBufferWritePos, _sfudBufferSize, _sfudBuffer); + _sfudBufferWritePos += _sfudBufferSize; + _sfudBufferPos = 0; + } + } + + void flushSfudBuffer() + { + if (_sfudBufferPos > 0) + { + sfud_erase_write(_flash, _sfudBufferWritePos, _sfudBufferSize, _sfudBuffer); + _sfudBufferWritePos += _sfudBufferSize; + _sfudBufferPos = 0; + } + } + + void writeSfudBuffer(byte *b, size_t len) + { + for (size_t i = 0; i < len; ++i) + { + writeSfudBuffer(b[i]); + } + } + +private: + byte *_sfudBuffer; + size_t _sfudBufferSize; + size_t _sfudBufferPos; + size_t _sfudBufferWritePos; + + const sfud_flash *_flash; +}; \ No newline at end of file diff --git a/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/language_understanding.h b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/language_understanding.h new file mode 100644 index 00000000..1c8d8653 --- /dev/null +++ b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/language_understanding.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +#include "config.h" + +class LanguageUnderstanding +{ +public: + int GetTimerDuration(String text) + { + DynamicJsonDocument doc(1024); + doc["text"] = text; + + String body; + serializeJson(doc, body); + + HTTPClient httpClient; + httpClient.begin(_client, TEXT_TO_TIMER_FUNCTION_URL); + + int httpResponseCode = httpClient.POST(body); + + int seconds = 0; + if (httpResponseCode == 200) + { + String result = httpClient.getString(); + Serial.println(result); + + DynamicJsonDocument doc(1024); + deserializeJson(doc, result.c_str()); + + JsonObject obj = doc.as(); + seconds = obj["seconds"].as(); + } + else + { + Serial.print("Failed to understand text - error "); + Serial.println(httpResponseCode); + } + + httpClient.end(); + + return seconds; + } + +private: + WiFiClient _client; +}; + +LanguageUnderstanding languageUnderstanding; \ No newline at end of file diff --git a/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/main.cpp b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/main.cpp new file mode 100644 index 00000000..a075f6cb --- /dev/null +++ b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/main.cpp @@ -0,0 +1,130 @@ +#include +#include +#include +#include +#include + +#include "config.h" +#include "language_understanding.h" +#include "mic.h" +#include "speech_to_text.h" +#include "text_to_speech.h" + +void connectWiFi() +{ + while (WiFi.status() != WL_CONNECTED) + { + Serial.println("Connecting to WiFi.."); + WiFi.begin(SSID, PASSWORD); + delay(500); + } + + Serial.println("Connected!"); +} + +void setup() +{ + Serial.begin(9600); + + while (!Serial) + ; // Wait for Serial to be ready + + delay(1000); + + connectWiFi(); + + while (!(sfud_init() == SFUD_SUCCESS)) + ; + + sfud_qspi_fast_read_enable(sfud_get_device(SFUD_W25Q32_DEVICE_INDEX), 2); + + pinMode(WIO_KEY_C, INPUT_PULLUP); + + mic.init(); + + speechToText.init(); + textToSpeech.init(); + + Serial.println("Ready."); +} + +auto timer = timer_create_default(); + +void say(String text) +{ + Serial.println(text); + textToSpeech.convertTextToSpeech(text); +} + +bool timerExpired(void *announcement) +{ + say((char *)announcement); + return false; +} + +void processAudio() +{ + String text = speechToText.convertSpeechToText(); + Serial.println(text); + + int total_seconds = languageUnderstanding.GetTimerDuration(text); + if (total_seconds == 0) + { + return; + } + + int minutes = total_seconds / 60; + int seconds = total_seconds % 60; + + String begin_message; + if (minutes > 0) + { + begin_message += minutes; + begin_message += " minute "; + } + if (seconds > 0) + { + begin_message += seconds; + begin_message += " second "; + } + + begin_message += "timer started."; + + String end_message("Times up on your "); + if (minutes > 0) + { + end_message += minutes; + end_message += " minute "; + } + if (seconds > 0) + { + end_message += seconds; + end_message += " second "; + } + + end_message += "timer."; + + say(begin_message); + + timer.in(total_seconds * 1000, timerExpired, (void *)(end_message.c_str())); +} + +void loop() +{ + if (digitalRead(WIO_KEY_C) == LOW && !mic.isRecording()) + { + Serial.println("Starting recording..."); + mic.startRecording(); + } + + if (!mic.isRecording() && mic.isRecordingReady()) + { + Serial.println("Finished recording"); + + processAudio(); + + mic.reset(); + } + + timer.tick(); +} diff --git a/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/mic.h b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/mic.h new file mode 100644 index 00000000..5f0815de --- /dev/null +++ b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/mic.h @@ -0,0 +1,242 @@ +#pragma once + +#include + +#include "config.h" +#include "flash_writer.h" + +class Mic +{ +public: + Mic() + { + _isRecording = false; + _isRecordingReady = false; + } + + void startRecording() + { + _isRecording = true; + _isRecordingReady = false; + } + + bool isRecording() + { + return _isRecording; + } + + bool isRecordingReady() + { + return _isRecordingReady; + } + + void init() + { + analogReference(AR_INTERNAL2V23); + + _writer.init(); + + initBufferHeader(); + configureDmaAdc(); + } + + void reset() + { + _isRecordingReady = false; + _isRecording = false; + + _writer.reset(); + + initBufferHeader(); + } + + void dmaHandler() + { + static uint8_t count = 0; + + if (DMAC->Channel[1].CHINTFLAG.bit.SUSP) + { + DMAC->Channel[1].CHCTRLB.reg = DMAC_CHCTRLB_CMD_RESUME; + DMAC->Channel[1].CHINTFLAG.bit.SUSP = 1; + + if (count) + { + audioCallback(_adc_buf_0, ADC_BUF_LEN); + } + else + { + audioCallback(_adc_buf_1, ADC_BUF_LEN); + } + + count = (count + 1) % 2; + } + } + +private: + volatile bool _isRecording; + volatile bool _isRecordingReady; + FlashWriter _writer; + +typedef struct + { + uint16_t btctrl; + uint16_t btcnt; + uint32_t srcaddr; + uint32_t dstaddr; + uint32_t descaddr; + } dmacdescriptor; + + // Globals - DMA and ADC + volatile dmacdescriptor _wrb[DMAC_CH_NUM] __attribute__((aligned(16))); + dmacdescriptor _descriptor_section[DMAC_CH_NUM] __attribute__((aligned(16))); + dmacdescriptor _descriptor __attribute__((aligned(16))); + + void configureDmaAdc() + { + // Configure DMA to sample from ADC at a regular interval (triggered by timer/counter) + DMAC->BASEADDR.reg = (uint32_t)_descriptor_section; // Specify the location of the descriptors + DMAC->WRBADDR.reg = (uint32_t)_wrb; // Specify the location of the write back descriptors + DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf); // Enable the DMAC peripheral + DMAC->Channel[1].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(TC5_DMAC_ID_OVF) | // Set DMAC to trigger on TC5 timer overflow + DMAC_CHCTRLA_TRIGACT_BURST; // DMAC burst transfer + + _descriptor.descaddr = (uint32_t)&_descriptor_section[1]; // Set up a circular descriptor + _descriptor.srcaddr = (uint32_t)&ADC1->RESULT.reg; // Take the result from the ADC0 RESULT register + _descriptor.dstaddr = (uint32_t)_adc_buf_0 + sizeof(uint16_t) * ADC_BUF_LEN; // Place it in the adc_buf_0 array + _descriptor.btcnt = ADC_BUF_LEN; // Beat count + _descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD | // Beat size is HWORD (16-bits) + DMAC_BTCTRL_DSTINC | // Increment the destination address + DMAC_BTCTRL_VALID | // Descriptor is valid + DMAC_BTCTRL_BLOCKACT_SUSPEND; // Suspend DMAC channel 0 after block transfer + memcpy(&_descriptor_section[0], &_descriptor, sizeof(_descriptor)); // Copy the descriptor to the descriptor section + + _descriptor.descaddr = (uint32_t)&_descriptor_section[0]; // Set up a circular descriptor + _descriptor.srcaddr = (uint32_t)&ADC1->RESULT.reg; // Take the result from the ADC0 RESULT register + _descriptor.dstaddr = (uint32_t)_adc_buf_1 + sizeof(uint16_t) * ADC_BUF_LEN; // Place it in the adc_buf_1 array + _descriptor.btcnt = ADC_BUF_LEN; // Beat count + _descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD | // Beat size is HWORD (16-bits) + DMAC_BTCTRL_DSTINC | // Increment the destination address + DMAC_BTCTRL_VALID | // Descriptor is valid + DMAC_BTCTRL_BLOCKACT_SUSPEND; // Suspend DMAC channel 0 after block transfer + memcpy(&_descriptor_section[1], &_descriptor, sizeof(_descriptor)); // Copy the descriptor to the descriptor section + + // Configure NVIC + NVIC_SetPriority(DMAC_1_IRQn, 0); // Set the Nested Vector Interrupt Controller (NVIC) priority for DMAC1 to 0 (highest) + NVIC_EnableIRQ(DMAC_1_IRQn); // Connect DMAC1 to Nested Vector Interrupt Controller (NVIC) + + // Activate the suspend (SUSP) interrupt on DMAC channel 1 + DMAC->Channel[1].CHINTENSET.reg = DMAC_CHINTENSET_SUSP; + + // Configure ADC + ADC1->INPUTCTRL.bit.MUXPOS = ADC_INPUTCTRL_MUXPOS_AIN12_Val; // Set the analog input to ADC0/AIN2 (PB08 - A4 on Metro M4) + while (ADC1->SYNCBUSY.bit.INPUTCTRL) + ; // Wait for synchronization + ADC1->SAMPCTRL.bit.SAMPLEN = 0x00; // Set max Sampling Time Length to half divided ADC clock pulse (2.66us) + while (ADC1->SYNCBUSY.bit.SAMPCTRL) + ; // Wait for synchronization + ADC1->CTRLA.reg = ADC_CTRLA_PRESCALER_DIV128; // Divide Clock ADC GCLK by 128 (48MHz/128 = 375kHz) + ADC1->CTRLB.reg = ADC_CTRLB_RESSEL_12BIT | // Set ADC resolution to 12 bits + ADC_CTRLB_FREERUN; // Set ADC to free run mode + while (ADC1->SYNCBUSY.bit.CTRLB) + ; // Wait for synchronization + ADC1->CTRLA.bit.ENABLE = 1; // Enable the ADC + while (ADC1->SYNCBUSY.bit.ENABLE) + ; // Wait for synchronization + ADC1->SWTRIG.bit.START = 1; // Initiate a software trigger to start an ADC conversion + while (ADC1->SYNCBUSY.bit.SWTRIG) + ; // Wait for synchronization + + // Enable DMA channel 1 + DMAC->Channel[1].CHCTRLA.bit.ENABLE = 1; + + // Configure Timer/Counter 5 + GCLK->PCHCTRL[TC5_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | // Enable perhipheral channel for TC5 + GCLK_PCHCTRL_GEN_GCLK1; // Connect generic clock 0 at 48MHz + + TC5->COUNT16.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ; // Set TC5 to Match Frequency (MFRQ) mode + TC5->COUNT16.CC[0].reg = 3000 - 1; // Set the trigger to 16 kHz: (4Mhz / 16000) - 1 + while (TC5->COUNT16.SYNCBUSY.bit.CC0) + ; // Wait for synchronization + + // Start Timer/Counter 5 + TC5->COUNT16.CTRLA.bit.ENABLE = 1; // Enable the TC5 timer + while (TC5->COUNT16.SYNCBUSY.bit.ENABLE) + ; // Wait for synchronization + } + + uint16_t _adc_buf_0[ADC_BUF_LEN]; + uint16_t _adc_buf_1[ADC_BUF_LEN]; + + // WAV files have a header. This struct defines that header + struct wavFileHeader + { + char riff[4]; /* "RIFF" */ + long flength; /* file length in bytes */ + char wave[4]; /* "WAVE" */ + char fmt[4]; /* "fmt " */ + long chunk_size; /* size of FMT chunk in bytes (usually 16) */ + short format_tag; /* 1=PCM, 257=Mu-Law, 258=A-Law, 259=ADPCM */ + short num_chans; /* 1=mono, 2=stereo */ + long srate; /* Sampling rate in samples per second */ + long bytes_per_sec; /* bytes per second = srate*bytes_per_samp */ + short bytes_per_samp; /* 2=16-bit mono, 4=16-bit stereo */ + short bits_per_samp; /* Number of bits per sample */ + char data[4]; /* "data" */ + long dlength; /* data length in bytes (filelength - 44) */ + }; + + void initBufferHeader() + { + wavFileHeader wavh; + + strncpy(wavh.riff, "RIFF", 4); + strncpy(wavh.wave, "WAVE", 4); + strncpy(wavh.fmt, "fmt ", 4); + strncpy(wavh.data, "data", 4); + + wavh.chunk_size = 16; + wavh.format_tag = 1; // PCM + wavh.num_chans = 1; // mono + wavh.srate = RATE; + wavh.bytes_per_sec = (RATE * 1 * 16 * 1) / 8; + wavh.bytes_per_samp = 2; + wavh.bits_per_samp = 16; + wavh.dlength = RATE * 2 * 1 * 16 / 2; + wavh.flength = wavh.dlength + 44; + + _writer.writeSfudBuffer((byte *)&wavh, 44); + } + + void audioCallback(uint16_t *buf, uint32_t buf_len) + { + static uint32_t idx = 44; + + if (_isRecording) + { + for (uint32_t i = 0; i < buf_len; i++) + { + int16_t audio_value = ((int16_t)buf[i] - 2048) * 16; + + _writer.writeSfudBuffer(audio_value & 0xFF); + _writer.writeSfudBuffer((audio_value >> 8) & 0xFF); + } + + idx += buf_len; + + if (idx >= BUFFER_SIZE) + { + _writer.flushSfudBuffer(); + idx = 44; + _isRecording = false; + _isRecordingReady = true; + } + } + } +}; + +Mic mic; + +void DMAC_1_Handler() +{ + mic.dmaHandler(); +} diff --git a/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/speech_to_text.h b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/speech_to_text.h new file mode 100644 index 00000000..a7ce075f --- /dev/null +++ b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/speech_to_text.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include +#include + +#include "config.h" +#include "flash_stream.h" + +class SpeechToText +{ +public: + void init() + { + _token_client.setCACert(TOKEN_CERTIFICATE); + _speech_client.setCACert(SPEECH_CERTIFICATE); + _access_token = getAccessToken(); + } + + String convertSpeechToText() + { + char url[128]; + sprintf(url, SPEECH_URL, SPEECH_LOCATION, LANGUAGE); + + HTTPClient httpClient; + httpClient.begin(_speech_client, url); + + httpClient.addHeader("Authorization", String("Bearer ") + _access_token); + httpClient.addHeader("Content-Type", String("audio/wav; codecs=audio/pcm; samplerate=") + String(RATE)); + httpClient.addHeader("Accept", "application/json;text/xml"); + + Serial.println("Sending speech..."); + + FlashStream stream; + int httpResponseCode = httpClient.sendRequest("POST", &stream, BUFFER_SIZE); + + Serial.println("Speech sent!"); + + String text = ""; + + if (httpResponseCode == 200) + { + String result = httpClient.getString(); + Serial.println(result); + + DynamicJsonDocument doc(1024); + deserializeJson(doc, result.c_str()); + + JsonObject obj = doc.as(); + text = obj["DisplayText"].as(); + } + else if (httpResponseCode == 401) + { + Serial.println("Access token expired, trying again with a new token"); + _access_token = getAccessToken(); + return convertSpeechToText(); + } + else + { + Serial.print("Failed to convert text to speech - error "); + Serial.println(httpResponseCode); + } + + httpClient.end(); + + return text; + } + +private: + String getAccessToken() + { + char url[128]; + sprintf(url, TOKEN_URL, SPEECH_LOCATION); + + HTTPClient httpClient; + httpClient.begin(_token_client, url); + + httpClient.addHeader("Ocp-Apim-Subscription-Key", SPEECH_API_KEY); + int httpResultCode = httpClient.POST("{}"); + + if (httpResultCode != 200) + { + Serial.println("Error getting access token, trying again..."); + delay(10000); + return getAccessToken(); + } + + Serial.println("Got access token."); + String result = httpClient.getString(); + + httpClient.end(); + + return result; + } + + WiFiClientSecure _token_client; + WiFiClientSecure _speech_client; + String _access_token; +}; + +SpeechToText speechToText; diff --git a/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/text_to_speech.h b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/text_to_speech.h new file mode 100644 index 00000000..d7174fcd --- /dev/null +++ b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/src/text_to_speech.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +class TextToSpeech +{ +public: + void init() + { + DynamicJsonDocument doc(1024); + doc["language"] = LANGUAGE; + + String body; + serializeJson(doc, body); + + HTTPClient httpClient; + httpClient.begin(_client, GET_VOICES_FUNCTION_URL); + + int httpResponseCode = httpClient.POST(body); + + if (httpResponseCode == 200) + { + String result = httpClient.getString(); + Serial.println(result); + + DynamicJsonDocument doc(1024); + deserializeJson(doc, result.c_str()); + + JsonArray obj = doc.as(); + _voice = obj[0].as(); + + Serial.print("Using voice "); + Serial.println(_voice); + } + else + { + Serial.print("Failed to get voices - error "); + Serial.println(httpResponseCode); + } + + httpClient.end(); + } + + void convertTextToSpeech(String text) + { + DynamicJsonDocument doc(1024); + doc["language"] = LANGUAGE; + doc["voice"] = _voice; + doc["text"] = text; + + String body; + serializeJson(doc, body); + + HTTPClient httpClient; + httpClient.begin(_client, TEXT_TO_SPEECH_FUNCTION_URL); + + int httpResponseCode = httpClient.POST(body); + + if (httpResponseCode == 200) + { + File wav_file = SD.open("SPEECH.WAV", FILE_WRITE); + httpClient.writeToStream(&wav_file); + wav_file.close(); + } + else + { + Serial.print("Failed to get speech - error "); + Serial.println(httpResponseCode); + } + + httpClient.end(); + } +private: + WiFiClient _client; + String _voice; +}; + +TextToSpeech textToSpeech; \ No newline at end of file diff --git a/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/test/README b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/test/README new file mode 100644 index 00000000..b94d0890 --- /dev/null +++ b/6-consumer/lessons/3-spoken-feedback/code-spoken-response/wio-terminal/smart-timer/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html diff --git a/6-consumer/lessons/3-spoken-feedback/code-timer/wio-terminal/smart-timer/src/config.h b/6-consumer/lessons/3-spoken-feedback/code-timer/wio-terminal/smart-timer/src/config.h index 5dc05537..4c06dc6a 100644 --- a/6-consumer/lessons/3-spoken-feedback/code-timer/wio-terminal/smart-timer/src/config.h +++ b/6-consumer/lessons/3-spoken-feedback/code-timer/wio-terminal/smart-timer/src/config.h @@ -16,7 +16,7 @@ const char *LANGUAGE = ""; const char *TOKEN_URL = "https://%s.api.cognitive.microsoft.com/sts/v1.0/issuetoken"; const char *SPEECH_URL = "https://%s.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1?language=%s"; -const char *TEXT_TO_TIMER_FUNCTION_URL = ""; +const char *TEXT_TO_TIMER_FUNCTION_URL = "http://:7071/api/text-to-timer"; const char *TOKEN_CERTIFICATE = "-----BEGIN CERTIFICATE-----\r\n" diff --git a/6-consumer/lessons/3-spoken-feedback/wio-terminal-set-timer.md b/6-consumer/lessons/3-spoken-feedback/wio-terminal-set-timer.md index 04026ad0..3480a4fd 100644 --- a/6-consumer/lessons/3-spoken-feedback/wio-terminal-set-timer.md +++ b/6-consumer/lessons/3-spoken-feedback/wio-terminal-set-timer.md @@ -49,13 +49,11 @@ Microcontrollers don't natively have support for multiple threads in Arduino, so LanguageUnderstanding languageUnderstanding; ``` -1. To call your functions app, you need to declare a WiFi client. +1. To call your functions app, you need to declare a WiFi client. Add the following to the `private` section of the class: - * If you are running the function app locally, add the following to the `private` section of the class: - - ```cpp - WiFiClient _client; - ``` + ```cpp + WiFiClient _client; + ``` 1. In the `public` section, declare a method called `GetTimerDuration` to call the functions app: diff --git a/6-consumer/lessons/3-spoken-feedback/wio-terminal-text-to-speech.md b/6-consumer/lessons/3-spoken-feedback/wio-terminal-text-to-speech.md index d7cd8725..bb247e19 100644 --- a/6-consumer/lessons/3-spoken-feedback/wio-terminal-text-to-speech.md +++ b/6-consumer/lessons/3-spoken-feedback/wio-terminal-text-to-speech.md @@ -98,7 +98,9 @@ Instead of downloading and decoding this entire list on your microcontroller, yo 1. Run your function app locally. You can then call this using a tool like curl in the same way that you tested your `text-to-timer` HTTP trigger. Make sure to pass your language as a JSON body: ```json - {"language":""} + { + "language":"" + } ``` Replace `` with your language, such as `en-GB`, or `zh-CN`. @@ -148,28 +150,11 @@ Instead of downloading and decoding this entire list on your microcontroller, yo TextToSpeech textToSpeech; ``` -1. To call your functions app, you need to declare a WiFi client. The type you need depends on whether you are accessing the function app locally or deployed to the cloud. - - * If you are running the function app locally, add the following to the `private` section of the class: - - ```cpp - WiFiClient _client; - ``` - - * If you are running the function app in the cloud, add the following to the `private` section of the class: - - ```cpp - WiFiClientSecure _client; - ``` - - You will also need to set the certificate on this class, so add the following constructor to the `public` section: +1. To call your functions app, you need to declare a WiFi client. Add the following to the `private` section of the class: - ```cpp - TextToSpeech() - { - _client.setCACert(FUNCTIONS_CERTIFICATE); - } - ``` + ```cpp + WiFiClient _client; + ``` 1. In the `private` section, add a field for the selected voice: @@ -469,6 +454,12 @@ When needing to manipulate data like this, it is often better to use serverless httpClient.end(); ``` +1. The text to be spoken can now be converted to audio. In the `main.cpp` file, add the following line to the end of the `say` function to convert the text to say into audio: + + ```cpp + textToSpeech.convertTextToSpeech(text); + ``` + ### Task - play audio from your Wio Terminal **Coming soon** diff --git a/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/include/README b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/include/README new file mode 100644 index 00000000..194dcd43 --- /dev/null +++ b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/lib/README b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/lib/README new file mode 100644 index 00000000..6debab1e --- /dev/null +++ b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/platformio.ini b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/platformio.ini new file mode 100644 index 00000000..8836ab42 --- /dev/null +++ b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/platformio.ini @@ -0,0 +1,23 @@ +; 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:seeed_wio_terminal] +platform = atmelsam +board = seeed_wio_terminal +framework = arduino +lib_deps = + seeed-studio/Seeed Arduino FS @ 2.1.1 + seeed-studio/Seeed Arduino SFUD @ 2.0.2 + seeed-studio/Seeed Arduino rpcWiFi @ 1.0.5 + seeed-studio/Seeed Arduino rpcUnified @ 2.1.3 + seeed-studio/Seeed_Arduino_mbedtls @ 3.0.1 + seeed-studio/Seeed Arduino RTC @ 2.0.0 + bblanchon/ArduinoJson @ 6.17.3 + contrem/arduino-timer @ 2.3.0 diff --git a/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/config.h b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/config.h new file mode 100644 index 00000000..9822eb2e --- /dev/null +++ b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/config.h @@ -0,0 +1,95 @@ +#pragma once + +#define RATE 16000 +#define SAMPLE_LENGTH_SECONDS 4 +#define SAMPLES RATE * SAMPLE_LENGTH_SECONDS +#define BUFFER_SIZE (SAMPLES * 2) + 44 +#define ADC_BUF_LEN 1600 + +const char *SSID = ""; +const char *PASSWORD = ""; + +const char *SPEECH_API_KEY = ""; +const char *SPEECH_LOCATION = ""; +const char *LANGUAGE = ""; +const char *SERVER_LANGUAGE = ""; + +const char *TOKEN_URL = "https://%s.api.cognitive.microsoft.com/sts/v1.0/issuetoken"; +const char *SPEECH_URL = "https://%s.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1?language=%s"; + +const char *TEXT_TO_TIMER_FUNCTION_URL = "http://:7071/api/text-to-timer"; +const char *GET_VOICES_FUNCTION_URL = "http://:7071/api/get-voices"; +const char *TEXT_TO_SPEECH_FUNCTION_URL = "http://:7071/api/text-to-speech"; +const char *TRANSLATE_FUNCTION_URL = "http://:7071/api/translate-text"; + +const char *TOKEN_CERTIFICATE = + "-----BEGIN CERTIFICATE-----\r\n" + "MIIF8zCCBNugAwIBAgIQAueRcfuAIek/4tmDg0xQwDANBgkqhkiG9w0BAQwFADBh\r\n" + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\n" + "d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\r\n" + "MjAeFw0yMDA3MjkxMjMwMDBaFw0yNDA2MjcyMzU5NTlaMFkxCzAJBgNVBAYTAlVT\r\n" + "MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKjAoBgNVBAMTIU1pY3Jv\r\n" + "c29mdCBBenVyZSBUTFMgSXNzdWluZyBDQSAwNjCCAiIwDQYJKoZIhvcNAQEBBQAD\r\n" + "ggIPADCCAgoCggIBALVGARl56bx3KBUSGuPc4H5uoNFkFH4e7pvTCxRi4j/+z+Xb\r\n" + "wjEz+5CipDOqjx9/jWjskL5dk7PaQkzItidsAAnDCW1leZBOIi68Lff1bjTeZgMY\r\n" + "iwdRd3Y39b/lcGpiuP2d23W95YHkMMT8IlWosYIX0f4kYb62rphyfnAjYb/4Od99\r\n" + "ThnhlAxGtfvSbXcBVIKCYfZgqRvV+5lReUnd1aNjRYVzPOoifgSx2fRyy1+pO1Uz\r\n" + "aMMNnIOE71bVYW0A1hr19w7kOb0KkJXoALTDDj1ukUEDqQuBfBxReL5mXiu1O7WG\r\n" + "0vltg0VZ/SZzctBsdBlx1BkmWYBW261KZgBivrql5ELTKKd8qgtHcLQA5fl6JB0Q\r\n" + "gs5XDaWehN86Gps5JW8ArjGtjcWAIP+X8CQaWfaCnuRm6Bk/03PQWhgdi84qwA0s\r\n" + "sRfFJwHUPTNSnE8EiGVk2frt0u8PG1pwSQsFuNJfcYIHEv1vOzP7uEOuDydsmCjh\r\n" + "lxuoK2n5/2aVR3BMTu+p4+gl8alXoBycyLmj3J/PUgqD8SL5fTCUegGsdia/Sa60\r\n" + "N2oV7vQ17wjMN+LXa2rjj/b4ZlZgXVojDmAjDwIRdDUujQu0RVsJqFLMzSIHpp2C\r\n" + "Zp7mIoLrySay2YYBu7SiNwL95X6He2kS8eefBBHjzwW/9FxGqry57i71c2cDAgMB\r\n" + "AAGjggGtMIIBqTAdBgNVHQ4EFgQU1cFnOsKjnfR3UltZEjgp5lVou6UwHwYDVR0j\r\n" + "BBgwFoAUTiJUIBiV5uNu5g/6+rkS7QYXjzkwDgYDVR0PAQH/BAQDAgGGMB0GA1Ud\r\n" + "JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMHYG\r\n" + "CCsGAQUFBwEBBGowaDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu\r\n" + "Y29tMEAGCCsGAQUFBzAChjRodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln\r\n" + "aUNlcnRHbG9iYWxSb290RzIuY3J0MHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6Ly9j\r\n" + "cmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5jcmwwN6A1oDOG\r\n" + "MWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5j\r\n" + "cmwwHQYDVR0gBBYwFDAIBgZngQwBAgEwCAYGZ4EMAQICMBAGCSsGAQQBgjcVAQQD\r\n" + "AgEAMA0GCSqGSIb3DQEBDAUAA4IBAQB2oWc93fB8esci/8esixj++N22meiGDjgF\r\n" + "+rA2LUK5IOQOgcUSTGKSqF9lYfAxPjrqPjDCUPHCURv+26ad5P/BYtXtbmtxJWu+\r\n" + "cS5BhMDPPeG3oPZwXRHBJFAkY4O4AF7RIAAUW6EzDflUoDHKv83zOiPfYGcpHc9s\r\n" + "kxAInCedk7QSgXvMARjjOqdakor21DTmNIUotxo8kHv5hwRlGhBJwps6fEVi1Bt0\r\n" + "trpM/3wYxlr473WSPUFZPgP1j519kLpWOJ8z09wxay+Br29irPcBYv0GMXlHqThy\r\n" + "8y4m/HyTQeI2IMvMrQnwqPpY+rLIXyviI2vLoI+4xKE4Rn38ZZ8m\r\n" + "-----END CERTIFICATE-----\r\n"; + +const char *SPEECH_CERTIFICATE = + "-----BEGIN CERTIFICATE-----\r\n" + "MIIF8zCCBNugAwIBAgIQCq+mxcpjxFFB6jvh98dTFzANBgkqhkiG9w0BAQwFADBh\r\n" + "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\r\n" + "d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\r\n" + "MjAeFw0yMDA3MjkxMjMwMDBaFw0yNDA2MjcyMzU5NTlaMFkxCzAJBgNVBAYTAlVT\r\n" + "MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKjAoBgNVBAMTIU1pY3Jv\r\n" + "c29mdCBBenVyZSBUTFMgSXNzdWluZyBDQSAwMTCCAiIwDQYJKoZIhvcNAQEBBQAD\r\n" + "ggIPADCCAgoCggIBAMedcDrkXufP7pxVm1FHLDNA9IjwHaMoaY8arqqZ4Gff4xyr\r\n" + "RygnavXL7g12MPAx8Q6Dd9hfBzrfWxkF0Br2wIvlvkzW01naNVSkHp+OS3hL3W6n\r\n" + "l/jYvZnVeJXjtsKYcXIf/6WtspcF5awlQ9LZJcjwaH7KoZuK+THpXCMtzD8XNVdm\r\n" + "GW/JI0C/7U/E7evXn9XDio8SYkGSM63aLO5BtLCv092+1d4GGBSQYolRq+7Pd1kR\r\n" + "EkWBPm0ywZ2Vb8GIS5DLrjelEkBnKCyy3B0yQud9dpVsiUeE7F5sY8Me96WVxQcb\r\n" + "OyYdEY/j/9UpDlOG+vA+YgOvBhkKEjiqygVpP8EZoMMijephzg43b5Qi9r5UrvYo\r\n" + "o19oR/8pf4HJNDPF0/FJwFVMW8PmCBLGstin3NE1+NeWTkGt0TzpHjgKyfaDP2tO\r\n" + "4bCk1G7pP2kDFT7SYfc8xbgCkFQ2UCEXsaH/f5YmpLn4YPiNFCeeIida7xnfTvc4\r\n" + "7IxyVccHHq1FzGygOqemrxEETKh8hvDR6eBdrBwmCHVgZrnAqnn93JtGyPLi6+cj\r\n" + "WGVGtMZHwzVvX1HvSFG771sskcEjJxiQNQDQRWHEh3NxvNb7kFlAXnVdRkkvhjpR\r\n" + "GchFhTAzqmwltdWhWDEyCMKC2x/mSZvZtlZGY+g37Y72qHzidwtyW7rBetZJAgMB\r\n" + "AAGjggGtMIIBqTAdBgNVHQ4EFgQUDyBd16FXlduSzyvQx8J3BM5ygHYwHwYDVR0j\r\n" + "BBgwFoAUTiJUIBiV5uNu5g/6+rkS7QYXjzkwDgYDVR0PAQH/BAQDAgGGMB0GA1Ud\r\n" + "JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMHYG\r\n" + "CCsGAQUFBwEBBGowaDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu\r\n" + "Y29tMEAGCCsGAQUFBzAChjRodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln\r\n" + "aUNlcnRHbG9iYWxSb290RzIuY3J0MHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6Ly9j\r\n" + "cmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5jcmwwN6A1oDOG\r\n" + "MWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RHMi5j\r\n" + "cmwwHQYDVR0gBBYwFDAIBgZngQwBAgEwCAYGZ4EMAQICMBAGCSsGAQQBgjcVAQQD\r\n" + "AgEAMA0GCSqGSIb3DQEBDAUAA4IBAQAlFvNh7QgXVLAZSsNR2XRmIn9iS8OHFCBA\r\n" + "WxKJoi8YYQafpMTkMqeuzoL3HWb1pYEipsDkhiMnrpfeYZEA7Lz7yqEEtfgHcEBs\r\n" + "K9KcStQGGZRfmWU07hPXHnFz+5gTXqzCE2PBMlRgVUYJiA25mJPXfB00gDvGhtYa\r\n" + "+mENwM9Bq1B9YYLyLjRtUz8cyGsdyTIG/bBM/Q9jcV8JGqMU/UjAdh1pFyTnnHEl\r\n" + "Y59Npi7F87ZqYYJEHJM2LGD+le8VsHjgeWX2CJQko7klXvcizuZvUEDTjHaQcs2J\r\n" + "+kPgfyMIOY1DMJ21NxOJ2xPRC/wAh/hzSBRVtoAnyuxtkZ4VjIOh\r\n" + "-----END CERTIFICATE-----\r\n"; diff --git a/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/flash_stream.h b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/flash_stream.h new file mode 100644 index 00000000..b841f1d0 --- /dev/null +++ b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/flash_stream.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include + +#include "config.h" + +class FlashStream : public Stream +{ +public: + FlashStream() + { + _pos = 0; + _flash_address = 0; + _flash = sfud_get_device_table() + 0; + + populateBuffer(); + } + + virtual size_t write(uint8_t val) + { + return 0; + } + + virtual int available() + { + int remaining = BUFFER_SIZE - ((_flash_address - HTTP_TCP_BUFFER_SIZE) + _pos); + int bytes_available = min(HTTP_TCP_BUFFER_SIZE, remaining); + + if (bytes_available == 0) + { + bytes_available = -1; + } + + return bytes_available; + } + + virtual int read() + { + int retVal = _buffer[_pos++]; + + if (_pos == HTTP_TCP_BUFFER_SIZE) + { + populateBuffer(); + } + + return retVal; + } + + virtual int peek() + { + return _buffer[_pos]; + } + +private: + void populateBuffer() + { + sfud_read(_flash, _flash_address, HTTP_TCP_BUFFER_SIZE, _buffer); + _flash_address += HTTP_TCP_BUFFER_SIZE; + _pos = 0; + } + + size_t _pos; + size_t _flash_address; + const sfud_flash *_flash; + + byte _buffer[HTTP_TCP_BUFFER_SIZE]; +}; diff --git a/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/flash_writer.h b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/flash_writer.h new file mode 100644 index 00000000..87fdff29 --- /dev/null +++ b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/flash_writer.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +class FlashWriter +{ +public: + void init() + { + _flash = sfud_get_device_table() + 0; + _sfudBufferSize = _flash->chip.erase_gran; + _sfudBuffer = new byte[_sfudBufferSize]; + _sfudBufferPos = 0; + _sfudBufferWritePos = 0; + } + + void reset() + { + _sfudBufferPos = 0; + _sfudBufferWritePos = 0; + } + + void writeSfudBuffer(byte b) + { + _sfudBuffer[_sfudBufferPos++] = b; + if (_sfudBufferPos == _sfudBufferSize) + { + sfud_erase_write(_flash, _sfudBufferWritePos, _sfudBufferSize, _sfudBuffer); + _sfudBufferWritePos += _sfudBufferSize; + _sfudBufferPos = 0; + } + } + + void flushSfudBuffer() + { + if (_sfudBufferPos > 0) + { + sfud_erase_write(_flash, _sfudBufferWritePos, _sfudBufferSize, _sfudBuffer); + _sfudBufferWritePos += _sfudBufferSize; + _sfudBufferPos = 0; + } + } + + void writeSfudBuffer(byte *b, size_t len) + { + for (size_t i = 0; i < len; ++i) + { + writeSfudBuffer(b[i]); + } + } + +private: + byte *_sfudBuffer; + size_t _sfudBufferSize; + size_t _sfudBufferPos; + size_t _sfudBufferWritePos; + + const sfud_flash *_flash; +}; \ No newline at end of file diff --git a/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/language_understanding.h b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/language_understanding.h new file mode 100644 index 00000000..1c8d8653 --- /dev/null +++ b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/language_understanding.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +#include "config.h" + +class LanguageUnderstanding +{ +public: + int GetTimerDuration(String text) + { + DynamicJsonDocument doc(1024); + doc["text"] = text; + + String body; + serializeJson(doc, body); + + HTTPClient httpClient; + httpClient.begin(_client, TEXT_TO_TIMER_FUNCTION_URL); + + int httpResponseCode = httpClient.POST(body); + + int seconds = 0; + if (httpResponseCode == 200) + { + String result = httpClient.getString(); + Serial.println(result); + + DynamicJsonDocument doc(1024); + deserializeJson(doc, result.c_str()); + + JsonObject obj = doc.as(); + seconds = obj["seconds"].as(); + } + else + { + Serial.print("Failed to understand text - error "); + Serial.println(httpResponseCode); + } + + httpClient.end(); + + return seconds; + } + +private: + WiFiClient _client; +}; + +LanguageUnderstanding languageUnderstanding; \ No newline at end of file diff --git a/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/main.cpp b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/main.cpp new file mode 100644 index 00000000..3d2c6182 --- /dev/null +++ b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/main.cpp @@ -0,0 +1,133 @@ +#include +#include +#include +#include +#include + +#include "config.h" +#include "language_understanding.h" +#include "mic.h" +#include "speech_to_text.h" +#include "text_to_speech.h" +#include "text_translator.h" + +void connectWiFi() +{ + while (WiFi.status() != WL_CONNECTED) + { + Serial.println("Connecting to WiFi.."); + WiFi.begin(SSID, PASSWORD); + delay(500); + } + + Serial.println("Connected!"); +} + +void setup() +{ + Serial.begin(9600); + + while (!Serial) + ; // Wait for Serial to be ready + + delay(1000); + + connectWiFi(); + + while (!(sfud_init() == SFUD_SUCCESS)) + ; + + sfud_qspi_fast_read_enable(sfud_get_device(SFUD_W25Q32_DEVICE_INDEX), 2); + + pinMode(WIO_KEY_C, INPUT_PULLUP); + + mic.init(); + + speechToText.init(); + textToSpeech.init(); + + Serial.println("Ready."); +} + +auto timer = timer_create_default(); + +void say(String text) +{ + text = textTranslator.translateText(text, SERVER_LANGUAGE, LANGUAGE); + Serial.println(text); + textToSpeech.convertTextToSpeech(text); +} + +bool timerExpired(void *announcement) +{ + say((char *)announcement); + return false; +} + +void processAudio() +{ + String text = speechToText.convertSpeechToText(); + text = textTranslator.translateText(text, LANGUAGE, SERVER_LANGUAGE); + Serial.println(text); + + int total_seconds = languageUnderstanding.GetTimerDuration(text); + if (total_seconds == 0) + { + return; + } + + int minutes = total_seconds / 60; + int seconds = total_seconds % 60; + + String begin_message; + if (minutes > 0) + { + begin_message += minutes; + begin_message += " minute "; + } + if (seconds > 0) + { + begin_message += seconds; + begin_message += " second "; + } + + begin_message += "timer started."; + + String end_message("Times up on your "); + if (minutes > 0) + { + end_message += minutes; + end_message += " minute "; + } + if (seconds > 0) + { + end_message += seconds; + end_message += " second "; + } + + end_message += "timer."; + + say(begin_message); + + timer.in(total_seconds * 1000, timerExpired, (void *)(end_message.c_str())); +} + +void loop() +{ + if (digitalRead(WIO_KEY_C) == LOW && !mic.isRecording()) + { + Serial.println("Starting recording..."); + mic.startRecording(); + } + + if (!mic.isRecording() && mic.isRecordingReady()) + { + Serial.println("Finished recording"); + + processAudio(); + + mic.reset(); + } + + timer.tick(); +} diff --git a/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/mic.h b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/mic.h new file mode 100644 index 00000000..5f0815de --- /dev/null +++ b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/mic.h @@ -0,0 +1,242 @@ +#pragma once + +#include + +#include "config.h" +#include "flash_writer.h" + +class Mic +{ +public: + Mic() + { + _isRecording = false; + _isRecordingReady = false; + } + + void startRecording() + { + _isRecording = true; + _isRecordingReady = false; + } + + bool isRecording() + { + return _isRecording; + } + + bool isRecordingReady() + { + return _isRecordingReady; + } + + void init() + { + analogReference(AR_INTERNAL2V23); + + _writer.init(); + + initBufferHeader(); + configureDmaAdc(); + } + + void reset() + { + _isRecordingReady = false; + _isRecording = false; + + _writer.reset(); + + initBufferHeader(); + } + + void dmaHandler() + { + static uint8_t count = 0; + + if (DMAC->Channel[1].CHINTFLAG.bit.SUSP) + { + DMAC->Channel[1].CHCTRLB.reg = DMAC_CHCTRLB_CMD_RESUME; + DMAC->Channel[1].CHINTFLAG.bit.SUSP = 1; + + if (count) + { + audioCallback(_adc_buf_0, ADC_BUF_LEN); + } + else + { + audioCallback(_adc_buf_1, ADC_BUF_LEN); + } + + count = (count + 1) % 2; + } + } + +private: + volatile bool _isRecording; + volatile bool _isRecordingReady; + FlashWriter _writer; + +typedef struct + { + uint16_t btctrl; + uint16_t btcnt; + uint32_t srcaddr; + uint32_t dstaddr; + uint32_t descaddr; + } dmacdescriptor; + + // Globals - DMA and ADC + volatile dmacdescriptor _wrb[DMAC_CH_NUM] __attribute__((aligned(16))); + dmacdescriptor _descriptor_section[DMAC_CH_NUM] __attribute__((aligned(16))); + dmacdescriptor _descriptor __attribute__((aligned(16))); + + void configureDmaAdc() + { + // Configure DMA to sample from ADC at a regular interval (triggered by timer/counter) + DMAC->BASEADDR.reg = (uint32_t)_descriptor_section; // Specify the location of the descriptors + DMAC->WRBADDR.reg = (uint32_t)_wrb; // Specify the location of the write back descriptors + DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xf); // Enable the DMAC peripheral + DMAC->Channel[1].CHCTRLA.reg = DMAC_CHCTRLA_TRIGSRC(TC5_DMAC_ID_OVF) | // Set DMAC to trigger on TC5 timer overflow + DMAC_CHCTRLA_TRIGACT_BURST; // DMAC burst transfer + + _descriptor.descaddr = (uint32_t)&_descriptor_section[1]; // Set up a circular descriptor + _descriptor.srcaddr = (uint32_t)&ADC1->RESULT.reg; // Take the result from the ADC0 RESULT register + _descriptor.dstaddr = (uint32_t)_adc_buf_0 + sizeof(uint16_t) * ADC_BUF_LEN; // Place it in the adc_buf_0 array + _descriptor.btcnt = ADC_BUF_LEN; // Beat count + _descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD | // Beat size is HWORD (16-bits) + DMAC_BTCTRL_DSTINC | // Increment the destination address + DMAC_BTCTRL_VALID | // Descriptor is valid + DMAC_BTCTRL_BLOCKACT_SUSPEND; // Suspend DMAC channel 0 after block transfer + memcpy(&_descriptor_section[0], &_descriptor, sizeof(_descriptor)); // Copy the descriptor to the descriptor section + + _descriptor.descaddr = (uint32_t)&_descriptor_section[0]; // Set up a circular descriptor + _descriptor.srcaddr = (uint32_t)&ADC1->RESULT.reg; // Take the result from the ADC0 RESULT register + _descriptor.dstaddr = (uint32_t)_adc_buf_1 + sizeof(uint16_t) * ADC_BUF_LEN; // Place it in the adc_buf_1 array + _descriptor.btcnt = ADC_BUF_LEN; // Beat count + _descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD | // Beat size is HWORD (16-bits) + DMAC_BTCTRL_DSTINC | // Increment the destination address + DMAC_BTCTRL_VALID | // Descriptor is valid + DMAC_BTCTRL_BLOCKACT_SUSPEND; // Suspend DMAC channel 0 after block transfer + memcpy(&_descriptor_section[1], &_descriptor, sizeof(_descriptor)); // Copy the descriptor to the descriptor section + + // Configure NVIC + NVIC_SetPriority(DMAC_1_IRQn, 0); // Set the Nested Vector Interrupt Controller (NVIC) priority for DMAC1 to 0 (highest) + NVIC_EnableIRQ(DMAC_1_IRQn); // Connect DMAC1 to Nested Vector Interrupt Controller (NVIC) + + // Activate the suspend (SUSP) interrupt on DMAC channel 1 + DMAC->Channel[1].CHINTENSET.reg = DMAC_CHINTENSET_SUSP; + + // Configure ADC + ADC1->INPUTCTRL.bit.MUXPOS = ADC_INPUTCTRL_MUXPOS_AIN12_Val; // Set the analog input to ADC0/AIN2 (PB08 - A4 on Metro M4) + while (ADC1->SYNCBUSY.bit.INPUTCTRL) + ; // Wait for synchronization + ADC1->SAMPCTRL.bit.SAMPLEN = 0x00; // Set max Sampling Time Length to half divided ADC clock pulse (2.66us) + while (ADC1->SYNCBUSY.bit.SAMPCTRL) + ; // Wait for synchronization + ADC1->CTRLA.reg = ADC_CTRLA_PRESCALER_DIV128; // Divide Clock ADC GCLK by 128 (48MHz/128 = 375kHz) + ADC1->CTRLB.reg = ADC_CTRLB_RESSEL_12BIT | // Set ADC resolution to 12 bits + ADC_CTRLB_FREERUN; // Set ADC to free run mode + while (ADC1->SYNCBUSY.bit.CTRLB) + ; // Wait for synchronization + ADC1->CTRLA.bit.ENABLE = 1; // Enable the ADC + while (ADC1->SYNCBUSY.bit.ENABLE) + ; // Wait for synchronization + ADC1->SWTRIG.bit.START = 1; // Initiate a software trigger to start an ADC conversion + while (ADC1->SYNCBUSY.bit.SWTRIG) + ; // Wait for synchronization + + // Enable DMA channel 1 + DMAC->Channel[1].CHCTRLA.bit.ENABLE = 1; + + // Configure Timer/Counter 5 + GCLK->PCHCTRL[TC5_GCLK_ID].reg = GCLK_PCHCTRL_CHEN | // Enable perhipheral channel for TC5 + GCLK_PCHCTRL_GEN_GCLK1; // Connect generic clock 0 at 48MHz + + TC5->COUNT16.WAVE.reg = TC_WAVE_WAVEGEN_MFRQ; // Set TC5 to Match Frequency (MFRQ) mode + TC5->COUNT16.CC[0].reg = 3000 - 1; // Set the trigger to 16 kHz: (4Mhz / 16000) - 1 + while (TC5->COUNT16.SYNCBUSY.bit.CC0) + ; // Wait for synchronization + + // Start Timer/Counter 5 + TC5->COUNT16.CTRLA.bit.ENABLE = 1; // Enable the TC5 timer + while (TC5->COUNT16.SYNCBUSY.bit.ENABLE) + ; // Wait for synchronization + } + + uint16_t _adc_buf_0[ADC_BUF_LEN]; + uint16_t _adc_buf_1[ADC_BUF_LEN]; + + // WAV files have a header. This struct defines that header + struct wavFileHeader + { + char riff[4]; /* "RIFF" */ + long flength; /* file length in bytes */ + char wave[4]; /* "WAVE" */ + char fmt[4]; /* "fmt " */ + long chunk_size; /* size of FMT chunk in bytes (usually 16) */ + short format_tag; /* 1=PCM, 257=Mu-Law, 258=A-Law, 259=ADPCM */ + short num_chans; /* 1=mono, 2=stereo */ + long srate; /* Sampling rate in samples per second */ + long bytes_per_sec; /* bytes per second = srate*bytes_per_samp */ + short bytes_per_samp; /* 2=16-bit mono, 4=16-bit stereo */ + short bits_per_samp; /* Number of bits per sample */ + char data[4]; /* "data" */ + long dlength; /* data length in bytes (filelength - 44) */ + }; + + void initBufferHeader() + { + wavFileHeader wavh; + + strncpy(wavh.riff, "RIFF", 4); + strncpy(wavh.wave, "WAVE", 4); + strncpy(wavh.fmt, "fmt ", 4); + strncpy(wavh.data, "data", 4); + + wavh.chunk_size = 16; + wavh.format_tag = 1; // PCM + wavh.num_chans = 1; // mono + wavh.srate = RATE; + wavh.bytes_per_sec = (RATE * 1 * 16 * 1) / 8; + wavh.bytes_per_samp = 2; + wavh.bits_per_samp = 16; + wavh.dlength = RATE * 2 * 1 * 16 / 2; + wavh.flength = wavh.dlength + 44; + + _writer.writeSfudBuffer((byte *)&wavh, 44); + } + + void audioCallback(uint16_t *buf, uint32_t buf_len) + { + static uint32_t idx = 44; + + if (_isRecording) + { + for (uint32_t i = 0; i < buf_len; i++) + { + int16_t audio_value = ((int16_t)buf[i] - 2048) * 16; + + _writer.writeSfudBuffer(audio_value & 0xFF); + _writer.writeSfudBuffer((audio_value >> 8) & 0xFF); + } + + idx += buf_len; + + if (idx >= BUFFER_SIZE) + { + _writer.flushSfudBuffer(); + idx = 44; + _isRecording = false; + _isRecordingReady = true; + } + } + } +}; + +Mic mic; + +void DMAC_1_Handler() +{ + mic.dmaHandler(); +} diff --git a/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/speech_to_text.h b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/speech_to_text.h new file mode 100644 index 00000000..a7ce075f --- /dev/null +++ b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/speech_to_text.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include +#include + +#include "config.h" +#include "flash_stream.h" + +class SpeechToText +{ +public: + void init() + { + _token_client.setCACert(TOKEN_CERTIFICATE); + _speech_client.setCACert(SPEECH_CERTIFICATE); + _access_token = getAccessToken(); + } + + String convertSpeechToText() + { + char url[128]; + sprintf(url, SPEECH_URL, SPEECH_LOCATION, LANGUAGE); + + HTTPClient httpClient; + httpClient.begin(_speech_client, url); + + httpClient.addHeader("Authorization", String("Bearer ") + _access_token); + httpClient.addHeader("Content-Type", String("audio/wav; codecs=audio/pcm; samplerate=") + String(RATE)); + httpClient.addHeader("Accept", "application/json;text/xml"); + + Serial.println("Sending speech..."); + + FlashStream stream; + int httpResponseCode = httpClient.sendRequest("POST", &stream, BUFFER_SIZE); + + Serial.println("Speech sent!"); + + String text = ""; + + if (httpResponseCode == 200) + { + String result = httpClient.getString(); + Serial.println(result); + + DynamicJsonDocument doc(1024); + deserializeJson(doc, result.c_str()); + + JsonObject obj = doc.as(); + text = obj["DisplayText"].as(); + } + else if (httpResponseCode == 401) + { + Serial.println("Access token expired, trying again with a new token"); + _access_token = getAccessToken(); + return convertSpeechToText(); + } + else + { + Serial.print("Failed to convert text to speech - error "); + Serial.println(httpResponseCode); + } + + httpClient.end(); + + return text; + } + +private: + String getAccessToken() + { + char url[128]; + sprintf(url, TOKEN_URL, SPEECH_LOCATION); + + HTTPClient httpClient; + httpClient.begin(_token_client, url); + + httpClient.addHeader("Ocp-Apim-Subscription-Key", SPEECH_API_KEY); + int httpResultCode = httpClient.POST("{}"); + + if (httpResultCode != 200) + { + Serial.println("Error getting access token, trying again..."); + delay(10000); + return getAccessToken(); + } + + Serial.println("Got access token."); + String result = httpClient.getString(); + + httpClient.end(); + + return result; + } + + WiFiClientSecure _token_client; + WiFiClientSecure _speech_client; + String _access_token; +}; + +SpeechToText speechToText; diff --git a/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/text_to_speech.h b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/text_to_speech.h new file mode 100644 index 00000000..d7174fcd --- /dev/null +++ b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/text_to_speech.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +class TextToSpeech +{ +public: + void init() + { + DynamicJsonDocument doc(1024); + doc["language"] = LANGUAGE; + + String body; + serializeJson(doc, body); + + HTTPClient httpClient; + httpClient.begin(_client, GET_VOICES_FUNCTION_URL); + + int httpResponseCode = httpClient.POST(body); + + if (httpResponseCode == 200) + { + String result = httpClient.getString(); + Serial.println(result); + + DynamicJsonDocument doc(1024); + deserializeJson(doc, result.c_str()); + + JsonArray obj = doc.as(); + _voice = obj[0].as(); + + Serial.print("Using voice "); + Serial.println(_voice); + } + else + { + Serial.print("Failed to get voices - error "); + Serial.println(httpResponseCode); + } + + httpClient.end(); + } + + void convertTextToSpeech(String text) + { + DynamicJsonDocument doc(1024); + doc["language"] = LANGUAGE; + doc["voice"] = _voice; + doc["text"] = text; + + String body; + serializeJson(doc, body); + + HTTPClient httpClient; + httpClient.begin(_client, TEXT_TO_SPEECH_FUNCTION_URL); + + int httpResponseCode = httpClient.POST(body); + + if (httpResponseCode == 200) + { + File wav_file = SD.open("SPEECH.WAV", FILE_WRITE); + httpClient.writeToStream(&wav_file); + wav_file.close(); + } + else + { + Serial.print("Failed to get speech - error "); + Serial.println(httpResponseCode); + } + + httpClient.end(); + } +private: + WiFiClient _client; + String _voice; +}; + +TextToSpeech textToSpeech; \ No newline at end of file diff --git a/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/text_translator.h b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/text_translator.h new file mode 100644 index 00000000..0404fa39 --- /dev/null +++ b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/src/text_translator.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include + +#include "config.h" + +class TextTranslator +{ +public: + String translateText(String text, String from_language, String to_language) + { + DynamicJsonDocument doc(1024); + doc["text"] = text; + doc["from_language"] = from_language; + doc["to_language"] = to_language; + + String body; + serializeJson(doc, body); + + Serial.print("Translating "); + Serial.print(text); + Serial.print(" from "); + Serial.print(from_language); + Serial.print(" to "); + Serial.println(to_language); + + HTTPClient httpClient; + httpClient.begin(_client, TRANSLATE_FUNCTION_URL); + + int httpResponseCode = httpClient.POST(body); + + String translated_text = ""; + + if (httpResponseCode == 200) + { + translated_text = httpClient.getString(); + Serial.print("Translated: "); + Serial.println(translated_text); + } + else + { + Serial.print("Failed to translate text - error "); + Serial.println(httpResponseCode); + } + + httpClient.end(); + + return translated_text; + } + +private: + WiFiClient _client; +}; + +TextTranslator textTranslator; diff --git a/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/test/README b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/test/README new file mode 100644 index 00000000..b94d0890 --- /dev/null +++ b/6-consumer/lessons/4-multiple-language-support/code/wio-terminal/smart-timer/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Unit Testing and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/page/plus/unit-testing.html