parent
016f100c25
commit
0ff507e7f2
@ -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
|
@ -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 <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
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
|
@ -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
|
@ -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 = "<SSID>";
|
||||
const char *PASSWORD = "<PASSWORD>";
|
||||
|
||||
const char *SPEECH_API_KEY = "<API_KEY>";
|
||||
const char *SPEECH_LOCATION = "<LOCATION>";
|
||||
const char *LANGUAGE = "<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://<IP_ADDRESS>:7071/api/text-to-timer";
|
||||
const char *GET_VOICES_FUNCTION_URL = "http://<IP_ADDRESS>:7071/api/get-voices";
|
||||
const char *TEXT_TO_SPEECH_FUNCTION_URL = "http://<IP_ADDRESS>: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";
|
@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <sfud.h>
|
||||
|
||||
#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];
|
||||
};
|
@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <sfud.h>
|
||||
|
||||
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;
|
||||
};
|
@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <WiFiClient.h>
|
||||
|
||||
#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<JsonObject>();
|
||||
seconds = obj["seconds"].as<int>();
|
||||
}
|
||||
else
|
||||
{
|
||||
Serial.print("Failed to understand text - error ");
|
||||
Serial.println(httpResponseCode);
|
||||
}
|
||||
|
||||
httpClient.end();
|
||||
|
||||
return seconds;
|
||||
}
|
||||
|
||||
private:
|
||||
WiFiClient _client;
|
||||
};
|
||||
|
||||
LanguageUnderstanding languageUnderstanding;
|
@ -0,0 +1,130 @@
|
||||
#include <Arduino.h>
|
||||
#include <arduino-timer.h>
|
||||
#include <rpcWiFi.h>
|
||||
#include <sfud.h>
|
||||
#include <SPI.h>
|
||||
|
||||
#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();
|
||||
}
|
@ -0,0 +1,242 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#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();
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
|
||||
#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<JsonObject>();
|
||||
text = obj["DisplayText"].as<String>();
|
||||
}
|
||||
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;
|
@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <Seeed_FS.h>
|
||||
#include <SD/Seeed_SD.h>
|
||||
#include <WiFiClient.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
|
||||
#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<JsonArray>();
|
||||
_voice = obj[0].as<String>();
|
||||
|
||||
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;
|
@ -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
|
@ -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
|
@ -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 <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
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
|
@ -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
|
@ -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 = "<SSID>";
|
||||
const char *PASSWORD = "<PASSWORD>";
|
||||
|
||||
const char *SPEECH_API_KEY = "<API_KEY>";
|
||||
const char *SPEECH_LOCATION = "<LOCATION>";
|
||||
const char *LANGUAGE = "<LANGUAGE>";
|
||||
const char *SERVER_LANGUAGE = "<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://<IP_ADDRESS>:7071/api/text-to-timer";
|
||||
const char *GET_VOICES_FUNCTION_URL = "http://<IP_ADDRESS>:7071/api/get-voices";
|
||||
const char *TEXT_TO_SPEECH_FUNCTION_URL = "http://<IP_ADDRESS>:7071/api/text-to-speech";
|
||||
const char *TRANSLATE_FUNCTION_URL = "http://<IP_ADDRESS>: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";
|
@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <sfud.h>
|
||||
|
||||
#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];
|
||||
};
|
@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <sfud.h>
|
||||
|
||||
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;
|
||||
};
|
@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <WiFiClient.h>
|
||||
|
||||
#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<JsonObject>();
|
||||
seconds = obj["seconds"].as<int>();
|
||||
}
|
||||
else
|
||||
{
|
||||
Serial.print("Failed to understand text - error ");
|
||||
Serial.println(httpResponseCode);
|
||||
}
|
||||
|
||||
httpClient.end();
|
||||
|
||||
return seconds;
|
||||
}
|
||||
|
||||
private:
|
||||
WiFiClient _client;
|
||||
};
|
||||
|
||||
LanguageUnderstanding languageUnderstanding;
|
@ -0,0 +1,133 @@
|
||||
#include <Arduino.h>
|
||||
#include <arduino-timer.h>
|
||||
#include <rpcWiFi.h>
|
||||
#include <sfud.h>
|
||||
#include <SPI.h>
|
||||
|
||||
#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();
|
||||
}
|
@ -0,0 +1,242 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#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();
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
|
||||
#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<JsonObject>();
|
||||
text = obj["DisplayText"].as<String>();
|
||||
}
|
||||
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;
|
@ -0,0 +1,86 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <Seeed_FS.h>
|
||||
#include <SD/Seeed_SD.h>
|
||||
#include <WiFiClient.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
|
||||
#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<JsonArray>();
|
||||
_voice = obj[0].as<String>();
|
||||
|
||||
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;
|
@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <WiFiClient.h>
|
||||
|
||||
#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;
|
@ -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
|
Loading…
Reference in new issue