Merge branch 'main' into id-translation

pull/116/head
Mohammad Zulfikar 4 years ago committed by GitHub
commit 2c337441c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -4,10 +4,6 @@ on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
jobs:
build_and_deploy_job:

@ -0,0 +1,201 @@
# Wio Terminal
[সীড স্টুডিও](https://www.seeedstudio.com/Wio-Terminal-p-4509.html) এর Wio Terminal একটি আরডুইনো সাপোর্টেড মাইক্রোকন্ট্রোলার, যাতে ওয়াইফাই সংযোগ এবং কিছু সেন্সর ও অ্যাকচুয়েটর বিল্ট-ইন রয়েছে। এছাড়াও এর সাথে রয়েছে কিছু পোর্ট, অতিরিক্ত সেন্সর ও অ্যাকচুয়েটর সংযোগ এবং এটি নির্মাণ করা হয়েছে একটি হার্ডওয়্যার ইকোসিস্টেম ব্যবহার করে যার নাম
[Grove](https://www.seeedstudio.com/category/Grove-c-1003.html).
![A Seeed studios Wio Terminal](../../../images/wio-terminal.png)
## সেটআপ
Wio Terminal ব্যবহার করার জন্য, আমাদের কিছু ফ্রি সটওয়্যার নিজেদের কম্পিউটার এ ইনস্টল করতে হবে। আমাদের অবশ্যই ওয়াইফাই সংযোগদানের পূর্বে Wio Terminal ফার্মওয়্যারটি আপডেট করে নিতে হবে।
### কাজের সেটআপ
প্রথমেই আমরা আমাদের প্রয়োজনীয় সটওয়্যারগুলো এবং ফার্মওয়ারটি আপডেট করে নেব।
১. ভিজুয়াল স্টুডিও কোড (ভি এস কোড) ইনস্টল করতে হবে । এটি একটি এডিটর যার সাহায্যে আমরা আমাদের ডিভাইস কোড লিখতে পারি সি/সি++ ভাষায়। বিস্তারিত জানতে [VS Code documentation](https://code.visualstudio.com?WT.mc_id=academic-17441-jabenn) টি পড়ে নেয়া যেতে পারে।
> 💁 আরডুইনো ডেভলপমেন্ট এর জন্য আর একটি ভালো আই.ডি.ই হলো [Arduino IDE](https://www.arduino.cc/en/software). এই IDE টির সাথে কাজ করার পূর্ব অভিজ্ঞতা থাকলে ভি এস কোড ও platformIO এর পরিবর্তে একেও ব্যাবহার করা যেতে পারে। তবে, এখানে আমরা ভি এস কোডের উপর ভিত্তি করেই কাজ করবো।
২. এরপর ভি এস কোড platformIO এক্সটেনশনটি ইনস্টল করতে হবে। এই এক্সটেনশনটি ভি এস কোডে ইনস্টল করতে [PlatformIO extension documentation](https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide&WT.mc_id=academic-17441-jabenn) এ দেওয়া দিকির্দেশনাগুলো পড়ে দেখতে পারেন। এটি একটি ভি এস কোড এক্সটেনশন যা সি/সি++ ভাষায় মাইক্রোকন্ট্রোলার প্রোগ্রামিংকে সাপোর্ট করে। এই এক্সটেনশনটি মাইক্রোসফট সি/সি++ এর উপর নির্ভর করে , সি অথবা সি++ ভাষা নিয়ে কাজ করার জন্য। উল্লেখ্য, এই সি/সি++ এক্সটেনশন সয়ংক্রিয়ভাবে ইনস্টল হয়ে যায় যখন কেউ platformIO ইনস্টল করে।
1. এখন, আমরা আমাদের Wio Terminal কে কম্পিউটার এর সাথে সংযুক্ত করব। এটির নিচের দিকে একটি ইউএসবি-সি পোর্ট আছে, সেটিকে আমরা আমাদের কম্পিউটার এর ইউএসবি পোর্ট এর সাথে সংযোগ দিব। উইও টার্মিনালে ইউএসবি-সি ও ইউএসবি-এ ক্যাবল থাকে। যদি আমাদের কম্পিউটারে শুধু ইউএসবি-সি পোর্ট থেকে, তাহলে আমাদের হয় ইউএসবি-সি ক্যাবল অথবা ইউএসবি-এ ক্যাবলের প্রয়োজন হবে ইউএসবি-সি অ্যাডাপ্টার এ সংযোগ দেওয়ার জন্য।
1. [Wio Terminal Wiki WiFi Overview documentation](https://wiki.seeedstudio.com/Wio-Terminal-Network-Overview/) এ উল্লেখিত দিকনির্দেশনা গুলোকে মেনে আমরা আমাদের উইও টার্মিনাল সেটআপ ও ফার্মওয়্যার আপডেট করে ফেলি।
+## হ্যালো ওয়ার্ল্ড
প্রথাগতভাবে, কোনো নতুন প্রোগ্রামিং ল্যাঙ্গুয়েজ অথবা টেকনোলজি নিয়ে কাজ শুরু করার সময় আমরা একটি "Hello World" application লিখি, একটি ছোট application যা আউটপুট হিসেবে `"Hello World"` লেখাটি দেখায়। এতে করে আমরা বুঝি যে আমাদের প্রোগ্রামটিতে সকল টুল সঠিকভাবে কাজ করছে।
আমাদের Wio Terminal এর হেলো ওয়ার্ল্ড অ্যাপটি এটি নিশ্চিত করবে যে আমাদের ভিজুয়াল স্টুডিও কোড platformIO এর সাথে সঠিকভাবে ইনস্টল করা হয়েছে এবং এখন এটি microcontroller development এর জন্য প্রস্তুত।
### platformIO প্রজেক্ট তৈরী
আমাদের প্রথম কাজ হলো platformIO ব্যাবহার করে একটি নতুন প্রজেক্ট তৈরী করা যা Wio terminal এর জন্য কনফিগার করা।
#### কাজ- platformIO প্রজেক্ট তৈরী
একটি platformIO প্রজেক্ট তৈরী করি।
১. Wio terminal কে কম্পিউটারের সাথে সংযোগ দেই।
২. ভি এস কোড launch করি
৩. আমরা platformIO আইকনটি সাইড মেন্যু বারে দেখতে পাবো:
![The Platform IO menu option](../../../images/vscode-platformio-menu.png)
এই মেন্যু আইটেমটি সিলেক্ট করে, সিলেক্ট করি *PIO Home -> Open*
![The Platform IO open option](../../../images/vscode-platformio-home-open.png)
. Welcome স্ক্রীন থেকে **+ New Project** বাটনটিতে ক্লিক করি।
![The new project button](../../../images/vscode-platformio-welcome-new-button.png)
৫. প্রজেক্টটিকে *Project Wizard* এ configure করি
1. প্রজেক্টটিকে `nightlight` নাম দেই।
1. *Board* dropdown থেকে, `WIO` লিখে বোর্ডগুলোকে ফিল্টার করি, *Seeeduino Wio Terminal* সিলেক্ট করি।
1. Leave the *Framework* as *Arduino*
1. হয় *Use default location* কে টিক অবস্থায় ছেড়ে দেই অথবা সেটিকে টিক না দিয়ে আমাদের প্রজেক্টটির জন্য যেকোনো location সিলেক্ট করি।
1. **Finish** বাটনটিতে ক্লিক করি।
![The completed project wizard](../../../images/vscode-platformio-nightlight-project-wizard.png)
platformIO এখন wio terminal এর কোডগুলোকে compile করার জন্য প্রয়োজনীয় কম্পনেন্টস ডাউনলোড করে নেবে এবং আমাদের প্রজেক্টটি create করে নেবে। পুরো প্রক্রয়াটি সম্পন্ন হতে কয়েক মিনিট সময় লাগতে পারে।
### platformIO প্রজেক্টটি investigate করে দেখা
ভি এস কোড এক্সপ্লোরার আমাদের কিছু ফাইল এবং ফোল্ডার দেখাবে যা platformIO wizerd দ্বারা তৈরি হয়েছে।
#### ফোল্ডারস
* `.pio` - এই ফোল্ডারটি কিছু temporary ডাটা বহন করে যা platformIO এর প্রয়জন হতে পারে, যেমন: libraries অথবা compiled code, এটা delete করার সাথে সাথে আবার পুনঃনির্মিতো হয়। U আমরা প্রজেক্টটি কোনো সাইট 
* `.vscode` - এই ফোল্ডারটি ভি এস কোড ও platformIO দ্বারা ব্যবহৃত configuration গুলোকে বহন করে। এটা delete করার সাথে সাথে আবার পুনঃনির্মিতো হয়। প্রজেক্টটি কোনো সাইট যেমন GitHub এ share করতে এর কোনো সোর্স কোড কন্ট্রোল অ্যাড করতে হবে না।
* `include` - এই ফোল্ডারটি এক্সটার্নাল হেডার ফাইল বহনের জন্য রয়েছে যা আমাদের কোডে অতিরিক্ত library যোগের সময় দরকার হয়। আমাদের কাজগুলোতে আমরা এই ফোল্ডারটি ব্যাবহার করব না।
* `lib` - এই ফোল্ডারটি কিছু এক্সটার্নাল libraries বহন করবে যা আমরা আমাদের কোড থেকে কল করব। আমাদের কাজগুলোতে আমরা এই ফোল্ডারটি ব্যাবহার করব না।
* `src` - এই ফোল্ডারটি আমাদের main সোর্স কোডটিকে বহন করবে, যা কিনা একটি সিংগেল ফাইল - main.cpp
* `test` - এই ফোল্ডারটি সেই স্থান যেখানে আমরা আমাদের কোডের ইউনিট টেস্ট গুলোকে রাখবো।
#### ফাইলস
* `main.cpp` - src ফোল্ডারে অবস্থিত এই ফাইলটি আমাদের অ্যাপ্লিকেশন এর entry point হিসেবে কাজ করবে। আমরা ফাইলটি খুলে দেখব, এটি বহন করে:
```cpp
#include <Arduino.h>
void setup() {
// put your setup code here, to run once:
}
void loop() {
// put your main code here, to run repeatedly:
}
```
যখন ডিভাইসটি কাজ শুরু করে, Arduino framework টি সেটআপ ফাংশনটি একবার রান করে, এরপর নিরন্তর এটিকে রান করতে থেকে যতক্ষণ পর্যন্ত ডিভাইসটি বন্ধ না হয় 
* `.gitignore` - এটি সেই ফাইল ও ডিরেক্টরিগুলোকে লিস্ট করে রাখে, যেগুলোকে আমরা আমাদের কোড git source code control এ যুক্ত করার সময় ইগনোর করবো, যেমন: কোনো GitHub repository তে আপলোড করার সময়।
* `platformio.ini` - এই ফাইলে আমাদের ডিভাইসের এবং অ্যাপের configuration গুলো রয়েছে । এটি খুললে দেখা যাবে: 
```ini
[env:seeed_wio_terminal]
platform = atmelsam
board = seeed_wio_terminal
framework = arduino
```
`[env:seeed_wio_terminal]` সেকশনটিতে wio terminal এর configuration আছে। আমরা একের অধিক `env` সেকশন রাখতে পারি যেন আমাদের কোডকে একের অধিক board এর জন্য compile করা যায়।
Project wizerd থেকে আরো কিছু value যা configuration ম্যাচ করে:
* `platform = atmelsam` Wio terminal যে হার্ডওয়্যারটি ব্যাবহার করে তাকে ডিফাইন করে (an ATSAMD51-based microcontroller)
* `board = seeed_wio_terminal` মাইক্রোকন্ট্রোলার এর টাইপ কে ডিফাইন করে (the Wio Terminal)
* `framework = arduino` আমাদের প্রজেক্টটি Arduino framework ব্যাবহার করে সেটি ডিফাইন করে।
### হ্যালো ওয়ার্ল্ড অ্যাপটি লিখি
এখন আমরা হ্যালো ওয়ার্ল্ড অ্যাপটি লিখার জন্য প্রস্তুত হয়েছি।
#### কাজ - হ্যালো ওয়ার্ল্ড অ্যাপটি লিখা
হ্যালো ওয়ার্ল্ড অ্যাপটি লিখি।
1. `main.cpp` ফাইলটি ভি এস কোড থেকে ওপেন করি।
1. কোডটি এমনভাবে লিখি যেনো এটি নিম্নোক্ত কোডটির সাথে মিলে যায়:
```cpp
#include <Arduino.h>
void setup()
{
Serial.begin(9600);
while (!Serial)
; // Wait for Serial to be ready
delay(1000);
}
void loop()
{
Serial.println("Hello World");
delay(5000);
}
```
`setup` ফাংশনটি একটি connection কে initialize করে সিরিয়াল পোর্ট এর সাথে, সেই usb পোর্টটি যেটি আমাদের কম্পিউটারকে wio terminal এর সাথে সংযুক্ত করেছে। `9600` প্যারামিটারটি হলো [baud rate](https://wikipedia.org/wiki/Symbol_rate) (যা সিম্বল রেট হিসেবেও পরিচিত) সিরিয়াল পোর্ট এর মধ্য দিয়ে যাওয়া ডাটার speed (bits per second). এই সেটিং দ্বারা আমরা বোঝাই ৯৬০০ bits ( এবং ১) ডাটা পাঠানো হচ্ছে প্রতি সেকেন্ডে। এরপর এটি সিরিয়াল পোর্টটি ready state এ যাওয়ার জন্য wait করে। 
+ `loop` ফাংশনটি `Hello World!` লাইনটির character গুলো এবং একটি new line character সিরিয়াল পোর্টে পাঠায়। এরপর, এটি ৫০০০ মিলি সেকেন্ড সময়ের জন্য sleep state এ যায়। Loop শেষ হওয়ার পর, এটি আবার রান করে এবং চলতে থাকে যতক্ষণ পর্যন্ত মাইক্রোকন্ট্রোলারটি ON থাকে।
1. কোডটি বিল্ড করে wio terminal এ আপলোড করি
1. ভি এস কোড command palette ওপেন করি।
1. 1. টাইপ করি `PlatformIO Upload` আপলোড অপশনটি খুঁজে পাওয়ার জন্য, এরপর *PlatformIO: Upload* সিলেক্ট করি।
![The PlatformIO upload option in the command palette](../../../images/vscode-platformio-upload-command-palette.png)
যদি দরকার হয়, platformIO এখন অটোমেটিক ভাবে কোডটিকে বিল্ড করবে, আপলোড করার পূর্বে।
1. কোডটি কম্পাইল হয়ে wio terminal এ আপলোড হয়ে যাবে 
> 💁 আমরা যদি MacOS ব্যাবহার করে থাকি, একটি *DISK NOT EJECTED PROPERLY* notification দেখতে পাবো। এটা এজন্যে দেখায় যে, wio terminal টি মাউন্টেড হয় ড্রাইভ হিসেবে যা কিনা ফ্লাশিং প্রসেসের একটি পার্ট, এবং এটি বিচ্ছিন্ন হয়ে যায় যখন compiled code টি আমদর ডিভাইস এ লেখা। আমরা এই নোটিফিকেশনটি ইগনোর করতে পারি।
⚠️ আমরা যদি error দেখতে পাই যে আপলোড পোর্ট unavailable, প্রথমত, আমাদের দেখতে হবে wio টার্মিনালটি আমাদের কম্পিউটারের সাথে সংযুক্ত আছে কিনা এবং স্ক্রীন এর বামদিকের সুইচটি অন করা আছে কিনা। নিচের দিকের সবুজ লাইটটি অন থাকতে হবে। এরপরও যদি error আসে, আমরা on/off সুইটটিকে দুবার নিচের দিকে টানবো এমনভাবে যেনো আমাদের wio terminal টি bootloader mode এ যায়। এরপর, আবার আপলোড করবো।
wio terminal এর একটি serial monitor থাকে যা wio terminal থেকে usb পোর্ট এর মাধ্যমে কতটুকু ডাটা পাঠানো হয়েছে তা দেখে। আমরা `Serial.println("Hello World");` কমান্ডটির মাধ্যমে কতটুকু ডাটা পাঠানো হয়েছে তা মনিটর করতে পারবো।
1. ভি এস কোড command palette ওপেন করি
1. `PlatformIO Serial` টাইপ করি serial monitor অপশনটি খুঁজে পাওয়া জন্য, সিলেক্ট *PlatformIO: Serial Monitor*
![The PlatformIO Serial Monitor option in the command palette](../../../images/vscode-platformio-serial-monitor-command-palette.png)
এখন একটি নতুন টার্মিনাল ওপেন হবে যেখানে সিরিয়াল পোর্টের মাধ্যমে যত ডাটা পাঠানো হয়েছে তা দেখা যাবে:
```output
> Executing task: platformio device monitor <
--- Available filters and text transformations: colorize, debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at http://bit.ly/pio-monitor-filters
--- Miniterm on /dev/cu.usbmodem101 9600,8,N,1 ---
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
Hello World
Hello World
```
serial monitor এ প্রতি ৫ সেকেন্ডে `Hello World` প্রিন্ট হবে।
> 💁 আমরা উক্ত কোডটি [code/wio-terminal](code/wio-terminal) ফোল্ডারে খুঁজে পাবো। 
😀 আমাদের 'হ্যালো ওয়ার্ল্ড' লেখাটি সফল হলো!!

@ -261,6 +261,7 @@ The challenge in the last lesson was to list as many IoT devices as you can that
* Read the [Arduino getting started guide](https://www.arduino.cc/en/Guide/Introduction) to understand more about the Arduino platform.
* Read the [introduction to the Raspberry Pi 4](https://www.raspberrypi.org/products/raspberry-pi-4-model-b/) to learn more about Raspberry Pis.
* Learn more on some of the concepts and acronyms in the [What the FAQ are CPUs, MPUs, MCUs, and GPUs article in the Electrical Engineering Journal](https://www.eejournal.com/article/what-the-faq-are-cpus-mpus-mcus-and-gpus/).
✅ Use these guides, along with the costs shown by following the links in the [hardware guide](../../../hardware.md) to decide on what hardware platform you want to use, or if you would rather use a virtual device.

@ -0,0 +1,264 @@
# IoT এর আরো গভীরে
![Embed a video here if available](video-url)
## লেকচার পূর্ববর্তী কুইজ
[লেকচার পূর্ববর্তী কুইজ](https://brave-island-0b7c7f50f.azurestaticapps.net/quiz/3)
## সূচনা
এই অংশে আমাদের আগের পাঠ্যের আলোচিত কিছু বেসিক ধারণাগুলির আরও গভী্রে যাব আমরা।
এই পাঠে আমরা কভার করব:
* [আইওটি উপাদানসমূহ](#আইওটি-উপাদানসমূহ)
* [মাইক্রোকন্ট্রোলারের আরো গভীরে](#মাইক্রোকন্ট্রোলারের-আরো-গভীরে)
* [সিংগেল বোর্ড কম্পিউটারের আরো গভীরে](#সিংগেল-বোর্ড-কম্পিউটারের-আরো-গভীরে)
## আইওটি উপাদানসমূহ
আইওটি অ্যাপ্লিকেশনের দুটি উপাদান হলো *ইন্টারনেট* এবং *থিংস* । এই দুটি উপাদানকে আরও কিছুটা বিস্তারিতভাবে দেখা যাক।
### থিংস
![A Raspberry Pi 4](../../../images/raspberry-pi-4.jpg)
***Raspberry Pi 4. Michael Henzler / [Wikimedia Commons](https://commons.wikimedia.org/wiki/Main_Page) / [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)***
**থিংস** বলতে আইওটির এই অংশটি এমন একটি ডিভাইসকে বোঝায় যা চারপাশের জগতের সাথে যোগাযোগ করতে পারে। এই ডিভাইসগুলি সাধারণত ছোট, কম দামের কম্পিউটার, কম গতিতে চলমান এবং কম শক্তি ব্যবহার করে। উদাহরণস্বরূপ সাধারণ মাইক্রোকন্ট্রোলারগুলি কিলোবাইট র‍্যামের (অথচ একটি পিসিতে তা গিগাবাইটের) চালিত হয় মাত্র কয়েক শতাধিক মেগাহার্টজ (অথচ একটি পিসিতে তা গিগাহার্টজের)। তবে কখনও কখনও এত অল্প শক্তি ব্যবহার করে তারা ব্যাটারিতে সপ্তাহ, মাস বা কয়েক বছর ধরে চলতে পারে।
এই যন্ত্রগুলো আমাদের চারপাশের পৃথিবীর সাথে সংযুক্ত থাকে; হয় সেন্সর ব্যবহার করে তথ্য সংগ্রহ করে অথবা একচুয়েটরের আউটপুট নিয়ন্ত্রণ করে কোন কাজ করার মাধ্যমে। এর সাধারণ একটি উদাহরণ হল স্মার্ট থার্মোস্ট্যাট -এমন একটি ডিভাইস যার মধ্যে তাপমাত্রা সেন্সর থাকে। এছাড়াও এতে থাকে একটি পছন্দসই তাপমাত্রা সেট করার উপায় যেমন ডায়াল বা টাচস্ক্রিন ব্যবহার করে এবং একটি তাপীকরণ বা শীতলকরণ ব্যবস্থার সাথে সংযুক্ত থাকে। ব্যবহারকারীর নির্ধারিত সীমার বাইরে গেলেই এই যন্ত্রগুলো চালু হয় । এখানে উদাহরণস্বরূপ, তাপমাত্রা সেন্সর সনাক্ত করে যে ঘরটি খুব শীতল এবং একটি একচুয়েটর তখন হিটিং চালু করে।
![A diagram showing temperature and a dial as inputs to an IoT device, and control of a heater as an output](../../../images/basic-thermostat.png)
***একটি সাধারণ থার্মোস্ট্যাট Temperature by Vectors Market / Microcontroller by Template / dial by Jamie Dickinson / heater by Pascal Heß - all from the [Noun Project](https://thenounproject.com)***
বিভিন্ন জিনিস রয়েছে যা আইওটি ডিভাইস হিসাবে কাজ করতে পারে, সংবেদনশীল ডেডিকেটেড হার্ডওয়্যার থেকে শুরু করে , জেনারেল পারপাস ডিভাইস এমনকি আমাদের স্মার্টফোন পর্যন্ত! একটি স্মার্টফোন চারপাশের বিভিন্ন তথ্য সংগ্রহের জন্য সেন্সর ব্যবহার করে এবং বাস্তব জগতের সাথে যোগাযোগ করে একচুয়েটর - উদাহরণস্বরূপ আমাদের অবস্থান সনাক্ত করতে জিপিএস সেন্সর এবং কোন গন্তব্যে আমাদেরকে নির্দেশনা দেওয়ার জন্য স্পিকার রয়েছে।
✅ আমাদের চারপাশের অন্যান্য সিস্টেমগুলির কথা চিন্তা করি যা সেন্সর থেকে ডেটা সংগ্রহ করে এবং সিদ্ধান্ত নিতে তা ব্যবহার করে। একটি উদাহরণ হতে পারে, ওভেনের উপর রাখা থার্মোস্ট্যাট। চারপাশে আরও কিছু কী খুঁজে পাওয়া যাবে ?
### ইন্টারনেট
আইওটি অ্যাপ্লিকেশনটির **ইন্টারনেট** অংশে বোঝান হয় এমন সব অ্যাপ্লিকেশন যার সাথে আইওটি ডিভাইসে সংযুক্ত থেকে ডেটা প্রেরণ এবং গ্রহণ করতে পারে। পাশাপাশি অন্যান্য অ্যাপ্লিকেশনগুলিও এর অংশ যা আইওটি ডিভাইস থেকে প্রাপ্ত ডেটা বিশ্লেষণ করতে পারে এবং আইওটি ডিভাইস একচুয়েটরকে কী কী নির্দেশ পাঠাতে হবে সেই সিদ্ধান্ত নেয়।
একটি সাধারণ সেটআপে আইওটি ডিভাইসটি সংযুক্ত হওয়ার সাথে কিছু ধরণের ক্লাউড সেবা থাকবে এবং এই ক্লাউড পরিষেবাগুলো সুরক্ষা, আইওটি ডিভাইস থেকে বার্তা গ্রহণ এবং ডিভাইসে বার্তা প্রেরণের মতো বিষয়গুলি পরিচালনা করে। এই ক্লাউড সার্ভিসটি তখন এমন অন্যান্য অ্যাপ্লিকেশনগুলির সাথে সংযুক্ত হবে যা সেন্সর ডেটা প্রক্রিয়া করতে বা স্টোর করতে পারে। এছাড়াও সিদ্ধান্ত নিতে অন্যান্য যেকোন সিস্টেমের ডেটার সেন্সর থেকে প্রাপ্ত ডেটা ক্লাউড সার্ভিস ব্যবহার করে থাকে।
ডিভাইসগুলো সবসময় যে ক্যাবল বা ওয়াইফাই দ্বারা সরাসরি ইন্টারনেটে সংযুক্ত থাকবে তাও কিন্তু নয়। কিছু যন্ত্র মেশ নেটওয়ার্ক ব্যবহার করে ব্লুটুথ বা এইধরণের কোন টেকনলজির সাহায্যে অন্য ডিভাইসের সাথে যুক্ত থাকে, আর এই সংযুক্তি ঘটায় হাব যা নিজে ইন্টারনেটের সাথে যুক্ত ।
ইন্টারনেট সংযোগে কাজের উদাহরণস্বরূপ, একটি থার্মোস্ট্যাট নিই, যা কিনা ক্লাউডে হোম ওয়াইফাই ব্যবহার করে সংযুক্ত হয়েছে। এটি এই ক্লাউড পরিষেবায় তাপমাত্রার ডেটা প্রেরণ করে এবং সেখান থেক তা কোন ডাটাবেইস বা তথ্যভান্ডারে সংরক্ষিত থাকে এবং বাড়ির মালিককে কোন একটি মোবাইল অ্যাপ্লিকেশন ব্যবহার করে বর্তমান এবং অতীত তাপমাত্রা যাচাই করার সুযোগ দেয়। ক্লাউডের অন্য একটি আগে থেকেই জেনে নেয় যে বাড়ির মালিক কত তাপমাত্রা পছন্দ করেন এবং সেই পছন্দের ভিত্তিতে ক্লাউড সার্ভিসের মাধ্যমে আইওটি ডিভাইসে বার্তা প্রেরণ করে হিটিং সিস্টেমটি চালু বা বন্ধ করতে বলে।
![A diagram showing temperature and a dial as inputs to an IoT device, the IoT device with 2 way communication to the cloud, which in turn has 2 way communication to a phone, and control of a heater as an output from the IoT device](../../../images/mobile-controlled-thermostat.png)
***একটি মোবাইল অ্যাপ্লিকেশন নিয়ন্ত্রিত, ইন্টারনেট সংযুক্ত থার্মোস্ট্যাট / Temperature by Vectors Market / Microcontroller by Template / dial by Jamie Dickinson / heater by Pascal Heß / mobile phone by Alice-vector / Cloud by Debi Alpa Nugraha - all from the [Noun Project](https://thenounproject.com)***
আরও উন্নত কোন ভার্সন, যা আইওটি ডিভাইসে সংযুক্ত অন্যান্য ডিভাইসের সেন্সরগুলির সাথে যেমন অকুপেন্সি সেন্সর থেকে বিভিন্ন তথ্য ( যেমন সেই সময়ের আবহাওয়া বা আপনার ব্যক্তিগত ক্যালেন্ডারে কী কী তথ্য রয়েছে) এর ভিত্তিতে সিদ্ধান্ত নিতে পারে যে তাপমাত্রা কত হওয়া উচিত। উদাহরণস্বরূপ আপনার ক্যালেন্ডারে বলা রয়েছে আজ আপনি ভ্রমণে গিয়েছেন। সেক্ষেত্রে শীতকালে আপনার রুমে হিটার চালানোর কোন দরকার নেই আর, আইওটি এই স্মার্ট ডিসিশনটি নিতে পারবে। এছাড়াও আপনি কোন রুম কখন কীভাবে ব্যবহার করেন, তার ভিত্তিতেও আর্টিফিশিয়াল ইন্টেলিজেন্স মডেলগুলি সিদ্ধান্ত নিতে পারে আর সময়ের সাথে সাথে প্রাপ্ত ডেটার কারণে এই সিদ্ধান্তগুলি আরো বেশি সঠিক হতে থাকে।
![A diagram showing multiple temperature sensors and a dial as inputs to an IoT device, the IoT device with 2 way communication to the cloud, which in turn has 2 way communication to a phone, a calendar and a weather service, and control of a heater as an output from the IoT device](../../../images/smarter-thermostat.png)
***একটি ইন্টারনেট সংযুক্ত থার্মোস্ট্যাট যা একাধিক রুমের সেন্সর ব্যবহার করে । এটি মোবাইল অ্যাপ্লিকেশন নিয়ন্ত্রিত এবং আবহাওয়া ও ক্যালেন্ডারের ডেটা থেকে বুদ্ধিমত্তা গ্রহণ করতে সক্ষম. Temperature by Vectors Market / Microcontroller by Template / dial by Jamie Dickinson / heater by Pascal Heß / mobile phone and Calendar by Alice-vector / Cloud by Debi Alpa Nugraha / smart sensor by Andrei Yushchenko / weather by Adrien Coquet - all from the [Noun Project](https://thenounproject.com)***
✅ ইন্টারনেট সংযুক্ত থার্মোস্ট্যাটকে আরও স্মার্ট করে তুলতে অন্য কোন কোন ধরণের ডেটা সাহায্য করতে পারে?
### Edge চালিত IoT
যদিও আইওটিতে **I** বলতে ইন্টারনেট বোঝায়, এই ডিভাইসগুলি যে অবশ্যই ইন্টারনেটে সংযুক্ত থাকতে হবে - তা পুরোপুরি সত্য নয়। কিছু ক্ষেত্রে আইওটি যন্ত্রগুলো 'এজ' ডিভাইসগুলির সাথে সংযোগ স্থাপন করতে পারে - যেগুলো হলো লোকাল নেটওয়ার্কে চালিত গেটওয়ে ডিভাইস যেখানে ইন্টারনেটে কোন সংযোগ না করেই ডেটা প্রক্রিয়া করতে পারবো। আমাদের যখন প্রচুর ডেটা ট্রান্সফার বা ধীর ইন্টারনেট সংযোগ থাকে তখন এটির মাধমে সম্পূর্ণ কাজ আরও দ্রুততর হতে পারে যা অফলাইনে চালানো যাবে এমনকি যখন কোন মানবিক সংকটের সময় কোন জাহাজে বা দুর্যোগ অঞ্চলে ইন্টারনেট সংযোগ সম্ভব হয়না, তখন সেখানে কার্যক্রম পরিচালনা করা যায়; সাথে সাথে আমাদের ব্যক্তিগত তথ্যের গোপনীয়তা রক্ষা করাও সম্ভব । কিছু ডিভাইসে ক্লাউড সুবিধা ব্যবহার করে তৈরি প্রসেসিং কোড থাকে এবং কোনও সিদ্ধান্ত নেওয়ার জন্য কোনও ইন্টারনেট সংযোগ ব্যবহার না করেই ডেটা সংগ্রহ ও প্রতিক্রিয়া জানাতে লোকাল নেটওয়ার্কে এটি চালানো যাবে।
উদাহরণস্বরূপ, আমাদের স্মার্ট হোম ডিভাইস যেমন অ্যাপল হোমপড, অ্যামাজন অ্যালেক্সা বা গুগল হোম যা প্রশিক্ষিত এআই মডেলগুলি ব্যবহার করে আমাদের ভয়েস শুনতে পাবে এবং নির্দিষ্ট শব্দ বা বাক্যাংশ বললে চালু হয় বা 'wake up' করে এবং তারপরই আমরা আমাদের কথাগুলো ইন্টারনেটে প্রসেসিং এর জন্য পাঠাই অথচ বাকি সময়ের নির্দেশগুলো প্রাইভেট থাকে। বিস্তারিত বলতে গেলে, ডিভাইসটি উপযুক্ত সময়ে আমাদের ভয়েস প্রেরণ বন্ধ করবে যেমন এটি যখন আমাদের কথায় কোন বিরতি সনাক্ত করে, তখন বন্ধ হয়ে যায়। এটিকে'wake up' করার আগে এবং ডিভাইসটি বন্ধ করার পরে আমরা যা কিছু বলছি, তা ইন্টারনেটের মাধ্যমে ডিভাইস সরবরাহকারীর বা প্রস্তুতকারকের কাছে প্রেরণ করা হবে না এবং তাই এটি ব্যক্তিগত গোপনীয়তা বজায় রাখবে।
✅ এমন কিছু পরিস্থিতির কথা চিন্তা করি যেখানে গোপনীয়তা গুরুত্বপূর্ণ, তাই ডেটা প্রক্রিয়াকরণটি ক্লাউডের চেয়ে 'Edge' এ করা তুলনামূলকভাবে ভালো । ছোট্ট একটি ইঙ্গিত দিই - ক্যামেরা বা অন্যান্য ইমেজিং ডিভাইস সম্বলিত আইওটি সার্ভিসকে এক্ষেত্রে ভাবা যেতে পারে ।
### IoT নিরাপত্তা
যেকোন ইন্টারনেট সংযোগের সাথে, সুরক্ষা একটি গুরুত্বপূর্ণ বিষয়। বেশ পরিচিত একটি কৌতুক রয়েছে যেখানে বলা হয়, IoT তে S মানে হলো Security - কিন্তু আইওটির পূর্ণরূপে কোথাও S নেই, যার মানে Security বা নিরাপত্তা নেই। উদাহরণস্বরূপ, [Stuxnet worm](https://wikipedia.org/wiki/Stuxnet) নামক ক্ষতিকারক 'কীট' বা worm অনেকগুলো সেন্ট্রিফিউজের ভাল্ভকে ভুলভাবে নিয়ন্ত্রণ করে কাজে ব্যাঘাত ঘটায়। হ্যাকাররাও তখন [baby monitor গুলোর নিম্নমানের নিরাপত্তার](https://www.npr.org/sections/thetwo-way/2018/06/05/617196788/s-c-mom-says-baby-monitor-was-hacked-experts-say-many-devices-are-vulnerable) সুযোগ নেয়।
> 💁 কখনও কখনও আইওটি ডিভাইস এবং Edge ডিভাইসগুলি ব্যক্তিগত তথ্য ও নিরাপত্তাকে সুরক্ষিত রাখতে ইন্টারনেট থেকে সম্পূর্ণ বিচ্ছিন্ন কোন নেটওয়ার্কে চালিত হয়। এটিকে বলা হয় [এয়ার গ্যাপিং](https://wikipedia.org/wiki/Air_gap_(networking))।
## মাইক্রোকন্ট্রোলারের আরো গভীরে
গত লেসনে আমরা মাইক্রোকন্ট্রোলারদের পরিচিত হয়েছিলাম। এখন তাদেরকে আরও গভীরভাবে জানবো।
### সিপিইউ
সিপিইউ হল মাইক্রোকন্ট্রোলারের 'মস্তিষ্ক'। এটি মূলত প্রসেসর যা আপনার কোড রান করে এবং কোন সংযুক্ত ডিভাইসে ডেটা প্রেরণ এবং তা থেকে ডেটা গ্রহণ করতে পারে। সিপিইউতে এক বা একাধিক কোর থাকতে পারে - যা মূলত এক বা একাধিক সিপিইউ যা কোড রান করার জন্য একসাথে কাজ করতে পারে।
সিপিইউগুলি একধরণের ঘড়ির উপর নির্ভর করে যা প্রতি সেকেন্ডে বহু মিলিয়ন বা বিলিয়ন বার টিক দেয়। প্রতিটি টিক বা সাইকেলে, সিপিইউর তার ক্ষমতানুসারে কাজগুলো করে। প্রতিটি টিকের সাথেই সিপিইউ কোন প্রোগ্রামের একটি নির্দেশনা কার্যকর করতে পারে, যেমন কোন বাহ্যিক ডিভাইস থেকে ডেটা পুনরুদ্ধার করা বা গাণিতিক গণনা সম্পাদন করা। এই নিয়মিত চক্রটি পরবর্তী নির্দেশাবলী প্রক্রিয়া করার আগেই আগের সব কাজ করে ফেলে।
ক্লক সাইকেল যত দ্রুত হবে, প্রতি সেকেন্ডে সিপিইউ তত বেশি কাজও করতে পারবে অর্থাৎ দ্রুততর সিপিইউ হবে। এদের গতি পরিমাপ করা হয়ে থাকে [হার্টজ (Hz)](https://wikipedia.org/wiki/Hertz) এককে, যেখানে ১ হার্টজ বলতে বোঝান হয়, প্রতি সেকেন্ডে একটি চক্র বা টিক সম্পাদন করা।
> 🎓 বেশিরভাগ সময় সিপিইউ স্পীড লেখা হয় MHz অথবা GHz দিয়ে। ১ মেগাহার্টজ হলো ১ মিলিয়ন হার্টজ এবং ১ গিগাহার্টজ হলো ১ বিলিয়ন হার্টজ ।
> 💁 সিপিইউগুলো [fetch-decode-execute cycle](https://wikipedia.org/wiki/Instruction_cycle) ব্যবহার করে প্রোগ্রাম এক্সেকিউট করে। প্রতি টিক এর সাথে সিপিইউ পরবর্তী নির্দেশনা গ্রহণ করবে, তা ডিকোড করবে এবং পরিশেষে এক্সেকিউট করবে যেমনঃ Arithmetic Logic Unit (ALU) ব্যবহার করে ২ যোগ করা। কিছু কিছু এক্সেকিউশন এর জন্য একাধিক টিক বা সাইকেল দরকার হয়। কাজ হয়ে যাওয়ার পর, পরবর্তী টিক আসলে তবেই পরের সাইকেলটি রান করবে।
![The fetch decode execute cycles showing the fetch taking an instruction from the program stored in RAM, then decoding and executing it on a CPU](../../../images/fetch-decode-execute.png)
***CPU by Icon Lauk / ram by Atif Arshad - all from the [Noun Project](https://thenounproject.com)***
মাইক্রোকন্ট্রোলারগুলির ক্লক স্পীড ডেস্কটপ বা ল্যাপটপ কম্পিউটার, এমনকি বেশিরভাগ স্মার্টফোনের চেয়ে অনেক কম। উদাহরণস্বরূপ, Wio টার্মিনালের একটি সিপিইউ রয়েছে যা 120MHz বা সেকেন্ডে 120,000,000 সাইকেল চালায়।
✅ একটি গড়পড়তা পিসি বা ম্যাক এর গিগাহার্টজে চলমান একাধিক কোর থাকে অর্থাৎ সেকেন্ডে কয়েক বিলিয়ন বার টিক দেয় বা সাইকেল সম্পাদন করে। আমাদের কম্পিউটারের ক্লক স্পীড কত তা জেনে নিয়ে Wio টার্মিনালের চেয়ে তা কতগুণ দ্রুত সেই হিসেব করি।
প্রতিটি সাইকেল রান করতে প্রয়োজন হয় শক্তি যা কিনা তাপ বা হিট তৈরী করে। যত দ্রুত ক্লকস্পীড, তত বেশি পাওয়ার প্রয়োজন হবে এবং তাপ উৎপন্ন হবে। পিসির তাপ অপসারণ করত 'হিট সিংক' এবং ফ্যান ব্যবহৃত হয় যা ছাড়া প্রচণ্ড উত্তাপের সাথে কয়েক সেকেন্ডের মধ্যেই কম্পিউটার বন্ধ হয়ে যাবে। মাইক্রোকন্ট্রোলারে এরকম তাপ অপসারণের কোন সুযোগ দরকার হয়না কেননা এরা অনেক কম গতিসম্পন্ন ক্লকস্পীডে চলে এবং তাই ততটা তাপ তৈরী করেনা।
> 💁 কিছু পিসি বা ম্যাক দ্রুত গতির হাই-পাওয়ার কোর এবং ধীর গতির লো-পাওয়ার কোর ব্যবহার শুরু করছে যাতে ব্যাটারি পাওয়ার বাঁচানো যায়। উদাহরণস্বরূপ, লেটেস্ট অ্যাপল ল্যাপটপে M1 চিপ ব্যবহার করে হচ্ছে, যা ৪টি পার্ফম্যান্স কোর এবং ৪টি ইফিশিয়েন্ট কোর এর মধ্যে কাজ ভাগ করতে পারে,যাতে করে পরিমিত ব্যাটারি লাইফ পাওয়া যায় আবার কাজের গতিও ঠিক থাকে।
✅ ছোট্ট একটা কাজ করিঃ সিপিইউ সম্পর্কে আরো একটু বিষদভাবে জানার চেষ্টা করি, এই [Wikipedia CPU article](https://wikipedia.org/wiki/Central_processing_unit) থেকে।
#### কাজ
Wio Terminal পর্যালোচনা করা ।
এই লেসনের জন্য আমরা Wio Terminal ব্যবহার করলে, এটার সিপিইউ কী খুঁজে দেখতে পারি ? [Wio Terminal প্রোডাক্ট পেইজ](https://www.seeedstudio.com/Wio-Terminal-p-4509.html) এ গিয়ে, একটু নিচে গেলেই *Hardware Overview* নামে একটি অংশ পাওয়া যাবে যেখানে Wio Terminal এর ভেতরের সব দেখা যায় - সিপিইউ আমরা সেখান থেকেই দেখতে পাবো।
### মেমোরি
মাইক্রোকন্ট্রোলারে সাধারণত ২ ধরণের মেমোরি থাকে - প্রোগ্রাম মেমোরি এবং র‍্যান্ডম একসেস মেমোরি (র‍্যাম)
প্রোগ্রাম মেমোরি অপরিবর্তনশীল, যার অর্থ এটিতে যা লেখা থাকে তা যখন ডিভাইসে পাওয়ার (ইলেক্ট্রিক সংযোগ) না থাকলেও, এটি ডিভাইসে স্টোর করা থাকে । আমাদের প্রোগ্রাম কোড মূলত এই মেমোরিতেই সংরক্ষিত থাকে ।
র‍্যাম ব্যবহার করে প্রোগ্রামগুলো চালানো হয় যা সেই সময়কালীন ভ্যারিয়েবল এবং প্রাপ্ত ডেটা স্টোর করে। র‍্যাম পরিবর্তনশীল, পাওয়ার (ইলেক্ট্রিক সংযোগ) বিচ্ছিন্ন হয়ে গেলে, এতে থাকা সব তথ্যও হারিয়ে যায় - বলতে গেলে পুরো প্রোগ্রামটাই প্রাথমিক অবস্থায় চলে আসে।
> 🎓 প্রোগ্রাম মেমোরি আমাদের কোড গুলো সংরক্ষণ করে থাকে যা পাওয়ার (ইলেক্ট্রিক সংযোগ) না থাকলেও, ডিভাইসে থেকে যায়।
> 🎓 র‍্যাম মূলত প্রোগ্রামকে রান করায় এবং পাওয়ার না থাকলে সবকিছু একদম শুরুর অবস্থায় চলে আসে।
মাইক্রোকন্ট্রোলারের মেমোরি, পিসি বা ম্যাকের তুলনায় নিতান্তই ক্ষুদ্র । একটা সাধারণ পিসি তে ৮ গিগাবাইট (জিবি) অর্থাৎ ৮০০০০০০০০০০ বাইট র‍্যাম থাকে, যার প্রতি বাইটে একটি অক্ষর বা থেকে ২৫৫ এর মধ্যে কোন সংখ্যা রাখা যায়। সেই তুলনায়, মাইক্রোকন্ট্রোলারে কিলোবাইট পর্যায়ের র‍্যাম থাকে, যা প্রায় ১০০০ বাইটের সমান। এখানে আমরা যে Wio terminal ব্যবহার করছি, তার র‍্যাম ১৯২ কিলোবাইট অর্থাৎ ১৯২০০০ বাইট - গড়পড়তা পিসির তুলনায় , গুণ কম।
নীচের চিত্রটি 192KB এবং 8GB এর মধ্যে আপেক্ষিক আকারের পার্থক্য দেখায় - কেন্দ্রের ছোট ডটটি 192KB উপস্থাপন করে।
![A comparison between 192KB and 8GB - more than 40,000 times larger](../../../images/ram-comparison.png)
প্রোগ্রাম মেমোরিও পিসির তুলনায় কম। একটি সাধারণ পিসিতে প্রোগ্রাম স্টোরেজের জন্য 500 গিগাবাইটের হার্ড ড্রাইভ থাকতে পারে, অন্যদিকে মাইক্রোকন্ট্রোলারের কাছে কেবল কিলোবাইট পর্যায়ের বা কয়েক মেগাবাইট (এমবি) স্টোরেজ থাকতে পারে (1 এমবি হলো 1000KB বা 1,000,000 বাইট এর সমান)। উইও টার্মিনালে 4MB প্রোগ্রাম স্টোরেজ রয়েছে।
✅ ছোট্ট একটা গবেষণা করা যাক : এই লেখা পড়তে যে কম্পিউটারটি ব্যবহার করছি তার র‍্যাম এবং কত স্টোরেজ ব্যবহার করে ত জানার চেষ্টা করি। কোন মাইক্রোকন্ট্রোলারের সাথে এটিকে কীভাবে তুলনা করা যায়?
### ইনপুট/আউটপুট
মাইক্রোকন্ট্রোলার সেন্সর থেকে ডেটা পড়তে এবং অ্যাকচুয়েটর দিয়ে নিয়ন্ত্রণ সংকেত প্রেরণের জন্য ইনপুট এবং আউটপুট সংযোগ প্রয়োজন। মাইক্রোকন্ট্রোলারে সাধারণত বেশ কয়েকটি জেনারেল-পারপাস ইনপুট / আউটপুট (জিপিআইও) পিন থাকে। এই পিনগুলিকে ইনপুট (সংকেত গ্রহণ) বা আউটপুট (সংকেত প্রেরণ) পিন হিসেবে কাজ করার জন্য সফ্টওয়্যারে কনফিগার করতে হবে ।
🧠⬅️ ইনপুট পিন দিয়ে সেন্সর থেকে তথ্য নেয়া হয়।
🧠➡️ আউটপুট পিন দিয়ে অ্যাকচুয়েটরে সংকেত পাঠানো হয়।
✅ পরবর্তী পাঠে এসব নিয়ে আমরা আরো বিস্তারিত জানতে পারবো।
#### কাজ
Wio Terminal পর্যালোচনা করি।
এই লেসনের জন্য আমরা Wio Terminal ব্যবহার করলে, এটার জিপিআইও পিনগুলো কী খুঁজে দেখতে পারি ? [Wio Terminal প্রোডাক্ট পেইজে](https://www.seeedstudio.com/Wio-Terminal-p-4509.html) একটু নিচে গেলেই *Pinout diagram* নামে একটি অংশ পাওয়া যাবে যেখানে আমরা সহজেই পিনগুলো দেখতে পারি। Wio Terminal এ পিন নাম্বার দেয়ার জন্য, স্টিকার দেয়া হয়ে থাকে - এগুলো ব্যবহার না করে থাকলে, দেরি না করে এখনই করে ফেলা যাক।
### বাস্তবিক আকার
মাইক্রোকন্ট্রোলার আকারে বেশ ছোট হয়, যেমন [Freescale Kinetis KL03 MCU ](https://www.edn.com/tiny-arm-cortex-m0-based-mcu-shrinks-package/) এর কথাই ধরা যাক - এতই ছোট যে গলফ বলের ডিম্পলের সমান। কোন পিসিতে থাকা সিপিইউ 40 মিমি x 40 মিমি আকারের এবং এটি (অতিরিক্ত গরম হয়ে কয়েক সেকেন্ডের বন্ধ হয়ে যাওয়া ঠেকাতে প্রয়োজনীয়) হিট-সিংক ও ফ্যান বাদ দিয়ে তারপরের হিসেব। বোঝাই যাচ্ছে, মাইক্রোকন্ট্রোলারের চেয়ে সিপিইউ কত বড়! মাইক্রোকন্ট্রোলার বাহ্যিক কেস, স্ক্রিন এবং বিভিন্ন সংযোগ এবং উপাদানগুলির সাথে থাকা Wio Terminal Developer kits একটি Intel i9 এর শুধুমাত্র সিপিইউয়ের চেয়ে তেমন একটা বড় নাহ - এবং হিট সিঙ্ক এবং ফ্যান সহ হিসেব করলে সিপিইউর চেয়ে অনেক ছোট !
| যন্ত্র | আকার |
| ------------------------------- | --------------------- |
| Freescale Kinetis KL03 | 1.6mm x 2mm x 1mm |
| Wio terminal | 72mm x 57mm x 12mm |
| Intel i9 CPU, Heat sink and fan | 136mm x 145mm x 103mm |
### ফ্রেমওয়ার্ক এবং অপারেটিং সিস্টেম
গতি এবং মেমরির আকারের কারণে, মাইক্রোকন্ট্রোলার ডেস্কটপ অর্থে কোন অপারেটিং সিস্টেম (ওএস) চালায় না। আমাদের কম্পিউটার (উইন্ডোজ, লিনাক্স বা ম্যাক-ওএস) চালিত অপারেটিং সিস্টেমের কাজগুলি চালনার জন্য প্রচুর মেমরি এবং প্রসেসিং পাওয়ার প্রয়োজন - মাইক্রোকন্ট্রোলারের জন্য যা সম্পূর্ণ অপ্রয়োজনীয় । মনে রাখতে হবে যে, মাইক্রোকন্ট্রোলার সাধারণত এক বা একাধিক নির্দিষ্ট কাজ সম্পাদনের জন্য প্রোগ্রাম করা হয়; যা পিসি বা ম্যাকের মতো জেনারেল পারপাস যন্ত্র নাহ। পিসি বা ম্যাক এ এমন ইন্টারফেস রাখতে হয় যা সঙ্গীত বা সিনেমা চালাতে পারে, ডকুমেন্টেশন বা কোড লেখার সরঞ্জাম সরবরাহ করতে পারে, গেম খেলতে পারে বা ইন্টারনেট ব্রাউজ করতে পারে - যা কিনা মাইক্রোকন্ট্রোলারের কাজের ধরণের তুলনায় অনেক আলাদা।
কোন ওএস ছাড়াই একটি মাইক্রোকন্ট্রোলার প্রোগ্রাম করার জন্য আমাদেরকে কোন ধরণের কোডিং করতে হবে যাতে মাইক্রোকন্ট্রোলার চলতে পারে বা API ব্যবহার করে যে কোনও পার্শ্ববর্তী কিছুর সাথে সংযোগ রাখতে পারে। প্রতিটি মাইক্রোকন্ট্রোলার আলাদা হয়, তাই নির্মাতারা সাধারণত স্ট্যান্ডার্ড ফ্রেমওয়ার্কগুলিকে সমর্থন করে যা আপনাকে আমাদের কোড তৈরির জন্য একটি স্ট্যান্ডার্ড 'রেসিপি' অনুসরণ করার সুযোগ করে এবং সেই ফ্রেমওয়ার্কটিকে সাপোর্ট করে এমন কোনও মাইক্রোকন্ট্রোলারে প্রোগ্রাম রান করে।
আমরা কোন একটি ওএস ব্যবহার করে মাইক্রোকন্ট্রোলারগুলিকে প্রোগ্রাম করতে পারি - প্রায়শই এগুলোকে রিয়েল-টাইম অপারেটিং সিস্টেম (আরটিওএস) হিসাবে উল্লেখ করা হয়, কারণ এগুলি রিয়েল টাইমে পেরিফেরিয়ালগুলিতে এবং পাঠানো ডেটা হ্যান্ডেল করার জন্য তৈরি করা হয়। এই অপারেটিং সিস্টেমগুলি খুব লাইটওয়েট এবং এদের বৈশিষ্ট্যগুলি হলো-
* মাল্টি থ্রেডিং, আপনার কোডগুলি একই সাথে একাধিক কোর বা একটি কোর পর্যায়ক্রমে ব্যবহার করে একাধিক ব্লক কোড চালানোর অনুমতি দেয়।
* নেটওয়ার্কিং - নিরাপদে ইন্টারনেটের মাধ্যমে যোগাযোগের অনুমতি দেওয়ার জন্য
* স্ক্রিন রয়েছে এমন ডিভাইসে ব্যবহারকারীর ইন্টারফেস (ইউআই) তৈরির জন্য গ্রাফিকাল ইউজার ইন্টারফেস (জিইউআই) এর উপস্থিতি।
✅ বিভিন্ন RTOS এর ব্যপারে জানতে এসব পড়তে পারি : [Azure RTOS](https://azure.microsoft.com/services/rtos/?WT.mc_id=academic-17441-jabenn), [FreeRTOS](https://www.freertos.org), [Zephyr](https://www.zephyrproject.org)
#### আরডুইনো
![The Arduino logo](../../../images/arduino-logo.svg)
[আরডুইনো](https://www.arduino.cc) খুব সম্ভবত সবচেয়ে জনপ্রিয় মাইক্রোকন্ট্রোলার ফ্রেমওয়ার্ক, বিশেষতঃ শিক্ষার্থী, শখের বশে আইওটিতে কাজ করতে আগ্রহীদের মাঝে। আরডুইনো একটি ওপেন সোর্স ইলেক্ট্রনিক্স প্ল্যাটফর্ম যা সফ্টওয়্যার এবং হার্ডওয়্যার সমন্বিত। আরডুইনো থেকে বা অন্য নির্মাতাদের কাছ থেকে আমরা আরডুইন সম্বলিত বোর্ড কিনে সেই ফ্রেমওয়ার্ক ব্যবহার করে কোড করতে পারবো।
আরডুইনো বোর্ডগুলি সি বা সি ++ এ কোড করা হয়। এই ভাষায় আমরা কোডগুলো খুব ছোট করে সংকলন করতে পারি এবং দ্রুত রান করতে পারি, যা একটি সীমাবদ্ধ ডিভাইসে যেমন মাইক্রোকন্ট্রোলারের জন্য বেশ গুরুত্বপূর্ণ। আরডুইনো অ্যাপ্লিকেশনটির মূল বিষয়কে স্কেচ হিসাবে উল্লেখ করা হয় এবং তা সি/সি++ এ কোড করা হয় মূলত ২টি ফাংশনে - `setup` এবং `loop`। বোর্ড চালু হয়ে গেলে, আরডুইনো ফ্রেমওয়ার্ক কোডটি একবার `setup` ফাংশনটি পরিচালনা করবে, তারপরে এটি `loop` ফাংশনটি বারবার রান করবে, পাওয়ার বন্ধ না হওয়া অবধি এটি অবিচ্ছিন্নভাবে চালিত হবে।
আমরা সেটআপ কোডটি `setup` ফাংশনে লিখবো, যেমন ওয়াইফাই এবং ক্লাউড সার্ভিসের সাথে সংযুক্ত হওয়া বা ইনপুট এবং আউটপুট জন্য পিন চালু হওয়া। আমাদের লুপ কোডটিতে তখন প্রসেসিং কোড থাকবে যেমন সেন্সর থেকে ডেটা নেয়া এবং ক্লাউডে তা পাঠানো । প্রতিটি লুপে সাধারণত একটি বিলম্ব (delay) অন্তর্ভুক্ত করতে হবে, উদাহরণস্বরূপ যদি আমরা কেবল 10 সেকেন্ড পরপর সেন্সর ডেটা প্রেরণ করতে চাই, তবে লুপের শেষে 10 সেকেন্ডের বিলম্ব যুক্ত করতে হবে, যাতে মাইক্রোকন্ট্রোলার তখন বিশ্রামে থাকে, শক্তি সঞ্চয় করে এবং তারপরে আবার 10 সেকেন্ড পরে যখন ডেটা প্রয়োজন হবে, তখন ল্যুপ চলবে।
![An arduino sketch running setup first, then running loop repeatedly](../../../images/arduino-sketch.png)
✅ এই প্রোগ্রাম আর্কিটেকচারকে বলা হয় *event loop* অথবা *message loop*. অনেক অ্যাপ্লিকেশন এটি ব্যবহার করে এবং উইন্ডোজ, ম্যাক-ওএস বা লিনাক্সের মতো ওএসে চালিত বেশিরভাগ ডেস্কটপ অ্যাপ্লিকেশনগুলির জন্য এটি স্ট্যান্ডার্ড। `loop` এখানে বাটনের মতো ব্যবহারকারী বা ইন্টারফেস উপাদান বা কীবোর্ডের মতো ডিভাইসগুলির নির্দেশনা গ্রহণ করে এবং সেই অনুযায়ে সাড়া দেয়। আরো বিস্তারিত জানতে [ইভেন্ট ল্যুপ](https://wikipedia.org/wiki/Event_loop) সংক্রান্ত লেখাটি পড়তে পারি।
আরডুইনো মাইক্রোকন্ট্রোলার এবং আই/ও পিনের সাথে সংযোগের জন্য স্ট্যান্ডার্ড লাইব্রেরি সরবরাহ করে, বিভিন্ন মাইক্রোকন্ট্রোলারগুলিতে চালনার যা সহায়তা করে। উদাহরণস্বরূপ, [`delay` function](https://www.arduino.cc/reference/en/language/functions/time/delay/) কোন প্রোগ্রামকে নির্দিষ্ট সময়ের জন্য বন্ধ রাখবে, [`digitalRead` function](https://www.arduino.cc/reference/en/language/functions/digital-io/digitalread/) বোর্ডের পিনগুলি থেকে `HIGH` অথবা `LOW` ডেটা সংগ্রহ করে, তা যে বোর্ডেই কোড রান করা হোক না কেন। এই স্ট্যান্ডার্ড লাইব্রেরিগুলির অর্থ একটি বোর্ডের জন্য লিখিত আরডুইনো কোড অন্য যে কোন আরডুইনো বোর্ডের জন্য পুনরায় ব্যবহার করা যেতে পারে এবং চলবে (পিনগুলি একই ধরে নিয়ে এবং বোর্ডগুলি একই বৈশিষ্ট্যগুলিকে সমর্থন কর - এমন হলে)
থার্ড-পার্টি আরডুইনো লাইব্রেরির একটি বড় সংগ্রহ রয়েছে যা আরডুইনো প্রকল্পগুলিতে অতিরিক্ত বৈশিষ্ট্য যুক্ত করার অনুমতি দেয় যেমন সেন্সর এবং অ্যাকচুয়েটর ব্যবহার করে বা ক্লাউড আইওটি সার্ভিসগুলিতে সংযুক্ত করা।
##### কাজ
Wio Terminal পর্যালোচনা করি।
এই লেসনের জন্য আমরা Wio Terminal ব্যবহার করলে, গত লেসনের কোডগুলো আবার একটু দেখি। [Wio Terminal প্রোডাক্ট পেইজে](https://www.seeedstudio.com/Wio-Terminal-p-4509.html) একটু নিচে গেলেই *Pinout diagram* নামে একটি অংশ পাওয়া যাবে যেখানে আমরা সহজেই পিনগুলো দেখতে পারি। Wio Terminal এ পিন নাম্বার দেয়ার জন্য, স্টিকার দেয়া হয়ে থাকে - এগুলো ব্যবহার না করে থাকলে, দেরি না করে এখনই করে ফেলা যাক।
## সিংগেল-বোর্ড কম্পিউটারের আরো গভীরে
শেষ লেসনে আমরা সিংগেল-বোর্ড কম্পিউটার এর সাথে পরিচিত হয়েছিলাম। এখন তাদের আরও গভীরভাবে জানবো।
### রাস্পবেরি পাই
![The Raspberry Pi logo](../../../images/raspberry-pi-logo.png)
[Raspberry Pi Foundation](https://www.raspberrypi.org) হলো মূলত স্কুল পর্যায়ে কম্পিউটার বিজ্ঞানের অধ্যয়নের প্রচারের জন্য ২০০৯ সালে প্রতিষ্ঠিত যুক্তরাজ্যের একটি দাতব্য সংস্থা। এই মিশনের অংশ হিসাবে তারা সিংগেল-বোর্ড কম্পিউটার তৈরী করে, যার নাম রাস্পবেরি পাই। এটি বর্তমানে ৩টি ভেরিয়েন্টে পাওয়া যা - একটি পূর্ণ আকারের সংস্করণ, ছোট পাই জিরো এবং একটি চূড়ান্ত মডিউল যা দিয়ে আমাদের আইওটি ডিভাইসে তৈরি করা যেতে পারে।
![A Raspberry Pi 4](../../../images/raspberry-pi-4.jpg)
***Raspberry Pi 4. Michael Henzler / [Wikimedia Commons](https://commons.wikimedia.org/wiki/Main_Page) / [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/)***
পূর্ণ আকারের রাস্পবেরি পাইয়ের সর্বশেষ ভার্সন হল Raspberry Pi 4B । এটিতে একটি কোয়াড-কোর (4 কোর) সিপিইউ রয়েছে যা 1.5GHz এবং 2, 4, বা 8 জিবি র‍্যাম, গিগাবিট ইথারনেট, ওয়াইফাই, 2টি এইচডিএমআই পোর্ট 4K স্ক্রিন সমর্থন করে, একটি অডিও এবং মিশ্রিত ভিডিও আউটপুট পোর্ট, ইউএসবি পোর্টস (USB 2.0, 2 USB 3.0 ভার্সন), 40টি জিপিআইও পিন, রাস্পবেরি পাই ক্যামেরা মডিউলটির জন্য একটি ক্যামেরা সংযোজক এবং একটি এসডি কার্ড স্লট। এই সমস্ত বোর্ড যা রয়েছে সব মিলিয়ে 88mm x 58mm x 19.5mm সাইজ এবং এটি একটি 3A USB-C পাওয়ার সাপ্লাই দ্বারা চালিত। রাস্পবেরি পাইয়ের দাম 35 মার্কিন ডলার থেকে শুরু হয়, যা পিসি বা ম্যাক এর তুলনায় অনেক কম।
> 💁 Pi400 নামে একটি "একের-ভিতর-সব" কম্পিউটার রয়েছে, যার কীবোর্ডে Pi4 বিল্ট-ইন রয়েছে।
![A Raspberry Pi Zero](../../../images/raspberry-pi-zero.jpg)
পাই জিরো এর আকার অনেক ছোট , যার পাওয়ার অনেক কম। এটিতে একটি একক কোর 1GHz সিপিইউ, 512 এমবি র‍্যাম, ওয়াইফাই (Zero W model এ ), একটি এইচডিএমআই পোর্ট, একটি মাইক্রো-ইউএসবি পোর্ট, 40টি জিপিআইও পিন, রাস্পবেরি পাই ক্যামেরা মডিউলটির সাথে একটি ক্যামেরা সংযোগকারী এবং একটি এসডি কার্ড স্লট রয়েছে।পাই এর আকার 65 মিমি x 30 মিমি x 5 মিমি এবং খুব অল্প পাওয়ার নিয়েই কাজ করতে পারে। পাই জিরো এর মূল্য 5 মার্কিন ডলার, আর ওয়াইফাই সহ , W ভার্সনটির দাম 10 মার্কিন ডলার।
> 🎓 এখানের সিপিইউ গুলো ARM processor এর, যা আমাদের পিসি বা ম্যাক এর রেগুলার Intel/AMD x86 বা x64 প্রসেসরের মতো নয়। তবে মাইক্রোকন্ট্রোলারের পাশাপাশি প্রায় সমস্ত মোবাইল ফোন, মাইক্রোসফ্ট সারফেস এক্স এবং নতুন অ্যাপল সিলিকন ভিত্তিক অ্যাপল ম্যাক এর সিপিইউগুলির সাথে এদের মিল রয়েছে।
রাস্পবেরি পাই এর সমস্ত ভ্যারিয়েন্ট রাস্পবেরি পাই ওএস নামে ডিবিয়ান লিনাক্সের একটি ভার্সন রান করে। এটি কোনও ডেস্কটপ ছাড়াই একটি ছোট ভার্সন হিসেবে পাওয়া যায় যা 'হেডলেস' প্রজেক্টগুলোর জন্য উপযুক্ত যেখানে ওয়েব ব্রাউজার, অফিস অ্যাপ্লিকেশন, কোডিং সরঞ্জাম এবং গেম চালানোর জন্য একটি স্ক্রিন বা একটি পূর্ণ ডেস্কটপ পরিবেশের কোন প্রয়োজনই নেই। এই ওএস হল ডেবিয়ান লিনাক্সের একটি সংস্করণ, যেটিতে আমরা ডেবিয়ানে চালিত কোন অ্যাপ্লিকেশন বা ট্যুল ইনস্টল করতে পারবো এবং এটি ARM প্রসেসরের জন্যই তৈরি।
#### কাজ
রাস্পবেরি পাই পর্যালোচনা
যদি এই লেসনটির জন্য আমরা রাস্পবেরি পাই ব্যবহার করি, তাহলে বোর্ডের বিভিন্ন হার্ডওয়্যার উপাদানগুলি সম্পর্কে ভালোভাবে জানতে হবে।
* প্রসেসর সম্পর্কিত ডিটেইলস [Raspberry Pi hardware documentation page](https://www.raspberrypi.org/documentation/hardware/raspberrypi/) এ পাওয়া যাবে। আমাদের ব্যবহার করা পাই এর প্রসেসর সম্পর্কে ঐ পেইজটি থেকে জানতে পারবো।
* GPIO পিনগুলো খুঁজে বের করি। [Raspberry Pi GPIO documentation](https://www.raspberrypi.org/documentation/hardware/raspberrypi/gpio/README.md)থেকে এদের ব্যাপারে আরো বিস্তারিত জানতে পারবো। [GPIO Pin Usage guide](https://www.raspberrypi.org/documentation/usage/gpio/README.md) টি পড়লে পাই এর বিভিন্ন পিন সম্পর্কে আমরা বিস্তারিত জানবো।
### সিংগেল-বোর্ড কম্পিউটারে প্রোগ্রামিং
সিংগেল-বোর্ড কম্পিউটারগুলিকে সম্পূর্ণ কম্পিউটার বলা যা্য, যা একটি সম্পূর্ণ ওএস এ রান করে। এর অর্থ হল যে অনেকগুলো প্রোগ্রামিং ভাষা, ফ্রেমওয়ার্ক এবং ট্যুল ব্যবহার করে কোডিং করা যাবে যা কিনা মাইক্রোকন্ট্রোলার যেমনঃ আরডুইনো তে সচরাচর করা যায়না কারণ বোর্ড থেকে সাপোর্টেড রয়েছে কিনা - এরকম বিষয়গুলি এখানে প্রভাব রাখে। বেশিরভাগ প্রোগ্রামিং ভাষার লাইব্রেরি রয়েছে যা সেন্সর এবং অ্যাকচুয়েটর থেকে যথাক্রমে ডেটা গ্রহণ এবং প্রেরণ করতে GPIO পিনগুলিতে ব্যবহার করতে পারে।
✅ আমরা কোন কোন প্রোগ্রামিং ভাষার সাথে পরিচিত? তারা কি লিনাক্স এ সাপোর্টেড ?
রাস্পবেরি পাইতে আইওটি অ্যাপ্লিকেশন তৈরির সর্বাধিক সাধারণ প্রোগ্রামিং ল্যাঙ্গুয়েজ হল পাইথন। পাইয়ের জন্য বানানো একটি হার্ডওয়ারের এক বিশাল ইকোসিস্টেম রয়েছে এবং এগুলির প্রায় সবগুলিতেই পাইথন লাইব্রেরি হিসাবে তাদের ব্যবহার করার জন্য প্রয়োজনীয় প্রাসঙ্গিক কোড অন্তর্ভুক্ত রয়েছে। এর মধ্যে কিছু ইকোসিস্টেম হল 'হ্যাট' এর উপর ভিত্তি করে - টুপির মতো একটি লেয়ার যা পাইয়ের উপরে বসে 40টি জিপিআইও পিনের একটি বড় সকেটের সাথে সংযুক্ত থাকে। এই টুপিগুলি অতিরিক্ত কিছু সুবিধা দেয় যেমনঃ স্ক্রিন, সেন্সর, রিমোট কন্ট্রোল কার অথবা অ্যাডাপ্টারগুলিকে সেন্সর যুক্ত করা যায় স্ট্যান্ডার্ড ক্যাবল ব্যবহার করেই।
### প্রফেশনাল পর্যায়ে আইওটি তৈরীর ক্ষেত্রে সিংগেল-বোর্ড কম্পিউটারের ব্যবহার
সিংগেল-বোর্ড কম্পিউটারগুলি কেবলমাত্র ডেভলাপার কিট হিসাবে নয়,বরং প্রফেশনাল পর্যায়ে আইওটি তৈরীর ক্ষেত্রেও ব্যবহৃত হয়। তারা হার্ডওয়্যার নিয়ন্ত্রণ এবং মেশিন লার্নিং মডেলগুলি চালানোর মতো জটিল কাজগুলি চালনার শক্তিশালী উপায় সরবরাহ করতে পারে। উদাহরণস্বরূপ, একটি [রাস্পবেরি পাই-4 কম্পিউট মডিউল](https://www.raspberrypi.org/blog/raspberry-pi-compute-module-4/) রয়েছে যা রাস্পবেরি পাই-4 এর সমস্ত পাওয়ার সরবরাহ করে, তবে তা বেশ কমপ্যাক্ট এবং কম মানের ফর্ম ফ্যাক্টরে যা কাস্টম হার্ডওয়্যারে ইনস্টল করার জন্য নকশাকৃত যাতে বেশিরভাগ পোর্ট নেই।
---
## 🚀 চ্যালেঞ্জ
গত লেসনের চ্যালেঞ্জটি ছিল বাড়ি, স্কুল বা কর্মক্ষেত্রে যতগুলি আইওটি ডিভাইস রয়েছে তার তালিকা করা। এই তালিকার প্রতিটি ডিভাইসের জন্য কী মাইক্রোকন্ট্রোলার বা সিংগেল-বোর্ড কম্পিউটার ব্যবহৃত হয় ? নাকি উভয়ের মিশ্রণের ফলেই এরা নির্মিত?
## লেকচার পরবর্তী কুইজ
[লেকচার পরবর্তী কুইজ](https://brave-island-0b7c7f50f.azurestaticapps.net/quiz/4)
## রিভিউ এবং স্ব-অধ্যয়ন
* [Arduino getting started guide](https://www.arduino.cc/en/Guide/Introduction) টি পড়ে আরডুইনো প্লাটফর্ম সম্পর্কে আরো জানতে হবে।
* [introduction to the Raspberry Pi 4](https://www.raspberrypi.org/products/raspberry-pi-4-model-b/) পড়ে রাস্পবেরি পাই সম্পর্কে আরো জানতে হবে।
✅ কোন হার্ডওয়্যার প্ল্যাটফর্মটি ব্যবহার করলে ভালো হবে বা শুধুমাত্র ভার্চুয়াল ডিভাইস ব্যবহার করবো কিনা তবে সিদ্ধান্ত নেওয়ার জন্য [হার্ডওয়্যার গাইডের](../../../hardware.md) লিংকগুলোতে প্রদত্ত খরচ এর তুলনা করতে হবে।
## এসাইনমেন্ট
[মাইক্রোকন্ট্রোলার এবং সিংগেল-বোর্ড কম্পিউটারের তুলনা করে পার্থক্য দাঁড় করানো](assignment.md)।

@ -0,0 +1,13 @@
# মাইক্রোকন্ট্রোলার এবং সিংগেল-বোর্ড কম্পিউটারের তুলনা করে পার্থক্য দাঁড় করানো
## নির্দেশনা
এই পাঠটিতে মাইক্রোকন্ট্রোলার এবং সিংগেল-বোর্ড কম্পিউটার নিয়ে আলোচনা হয়েছে । তাদের তুলনা করে এবং বিপরী্ত্য সম্বলিত একটি সারণী তৈরি করে কমপক্ষে ২টি কারণ লিখতে হবে যে কেন একটি সিংগেল-বোর্ড কম্পিউটারের পরিবর্তে মাইক্রোকন্ট্রোলার ব্যবহার করা উচিত। একইভাবে কমপক্ষে ২টি কারণ লিখতে হবে যে কেন একট মাইক্রোকন্ট্রোলারের পবিবর্তে সিংগেল-বোর্ড কম্পিউটার ব্যবহার করা উচিত।
## এসাইনমেন্ট মূল্যায়ন মানদন্ড
| ক্রাইটেরিয়া | দৃষ্টান্তমূলক ব্যখ্যা (সর্বোত্তম) | পর্যাপ্ত ব্যখ্যা (মাঝারি) | আরো উন্নতির প্রয়োজন (নিম্ন) |
| -------- | ---------------------- | ------------------- | ------------------------- |
| একক-বোর্ড কম্পিউটারের সাথে মাইক্রোকন্ট্রোলার এর তুলনা করে একটি সারণী তৈরি করা | একাধিক আইটেম সঠিকভাবে তুলনা এবং বৈপরীত্যসহ একটি তালিকা তৈরি করেছে | কেবল অল্প কয়েকটি বিষয় নিয়ে একটি তালিকা তৈরি করেছে | শুধুমাত্র একটি বা শুণ্যটি তুলনা এবং বৈপরীত্যসহ তালিকা তৈরি করেছে |
| একটির পরিবর্তে অন্যটি ব্যবিহারের কারণ | ২ বা ততোধিক কারণ প্রদর্শন করেছে | ১ বা ২টি কারণ প্রদর্শন করেছে | ১ বা ততোধিক কারণ প্রদর্শন করতে পারেনি |

@ -1,8 +1,8 @@
# Interact with the physical world with sensors and actuators
Add a sketchnote if possible/appropriate
![A sketchnote overview of this lesson](../../../sketchnotes/lesson-3.png)
![Embed a video here if available](video-url)
> Sketchnote by [Nitya Narasimhan](https://github.com/nitya). Click the image for a larger version.
## Pre-lecture quiz

@ -19,7 +19,7 @@ Connect the light sensor
![A grove light sensor](../../../images/grove-light-sensor.png)
1. Insert one end of a Grove cable into the socket on the light sensor module. It will only go in one way round.
1.
1. With the Raspberry Pi powered off, connect the other end of the Grove cable to the analog socket marked **A0** on the Grove Base hat attached to the Pi. This socket is the second from the right, on the row of sockets next to the GPIO pins.
![The grove light sensor connected to socket A0](../../../images/pi-light-sensor.png)

@ -1,9 +0,0 @@
# Dummy File
This file acts as a placeholder for the `translations` folder. <br>
**Please remove this file after adding the first translation**
For the instructions, follow the directives in the [translations guide](https://github.com/microsoft/IoT-For-Beginners/blob/main/TRANSLATIONS.md) .
## THANK YOU
We truly appreciate your efforts!

@ -0,0 +1,215 @@
# সেন্সর এবং অ্যাকচুয়েটরের সাহায্যে বাহ্যিক জগতের সাথে যোগাযোগ
## লেকচার পূর্ববর্তী কুইজ
[লেকচার পূর্ববর্তী কুইজ](https://brave-island-0b7c7f50f.azurestaticapps.net/quiz/5)
## পরিচিতি
এই লেসনটি আমাদের আইওটি ডিভাইসের জন্য দুটি গুরুত্বপূর্ণ ধারণার পরিচয় করিয়ে দেয় - সেন্সর এবং অ্যাকচুয়েটর । আইওটি প্রজেক্টে লাইট সেন্সর যুক্ত করে, তারপরে আলোর মাত্রা দ্বারা নিয়ন্ত্রিত একটি এলইডি সংযুক্ত করার মাধ্যমে কার্যকরভাবে একটি রাতের আলোকীয় যন্ত্র বা 'নাইটলাইট' তৈরি করা যাবে।
এই পাঠ্যে আমরা দেখবো :
* [সেন্সর কী?](#সেন্সর-কী)
* [একটি সেন্সর ব্যবহার](#একটি-সেন্সর-ব্যবহার)
* [সেন্সর কত প্রকার](#সেন্সর-কত-প্রকার)
* [অ্যাকচুয়েটর কী?](#অ্যাকচুয়েটর-কী)
* [একটি অ্যাকচুয়েটর ব্যবহার](#একটি-অ্যাকচুয়েটর-ব্যবহার)
* [অ্যাকচুয়েটর কত প্রকার](#অ্যাকচুয়েটর-কত-প্রকার)
## সেন্সর কী?
সেন্সরগুলি এমন একটি হার্ডওয়্যার ডিভাইস যা বাহ্যিক জগতকে বুঝতে পারে - অর্থাৎ তারা তাদের চারপাশে এক বা একাধিক বৈশিষ্ট্য পরিমাপ করে এবং তথ্যগুলো আইওটি ডিভাইসে প্রেরণ করে। প্রাকৃতিক বৈশিষ্ট্য যেমন বায়ু তাপমাত্রা থেকে শুরু করে শারীরিক মিথস্ক্রিয়া যেমন চলাফেরার মতো প্রাকৃতিক বৈশিষ্ট্য থেকে পরিমাপ করা যায় এমন অনেকগুলি জিনিস রয়েছে বলে সেন্সরগুলি বিস্তৃত ডিভাইস কভার করে।
সেন্সর দ্বারা অনেকগুলো বিষয় পরিমাপ করা সম্ভব; প্রাকৃতিক বৈশিষ্ট্য যেমন বায়ু তাপমাত্রা থেকে শুরু করে শারীরিক মিথস্ক্রিয়া যেমন চলাফেরার মতো অনেককিছুতেই এটির ব্যবহার লক্ষ্যণীয় ।
কিছু অতি ব্যবহৃত সেন্সর হলো:
* তাপমাত্রা সেন্সর - এগুলি বায়ুর তাপমাত্রা বা যে মাধ্যমে নিমজ্জিত রয়েছে সেটির তাপমাত্রা নির্ণয় করতে পারে। শৌখিন এবং প্রফেশনাল ডেভলাপারদের জন্য প্রায়ই এই একটি সেন্সরেই বায়ুচাপ এবং আর্দ্রতার ও নির্ণয়ের সুবিধা প্রদান করা হয়।
* বাটন - কেউ যদি বাটনে প্রেস করে তখন তারা তা বুঝতে পারে।
* আলোকীয় সেন্সর - এগুলি আলোর মাত্রা সনাক্ত করে এবং নির্দিষ্ট রঙ, ইউভি আলো, আইআর লাইট বা সাধারণ দৃশ্যমান আলোর জন্য সুনির্দিষ্টভাবে কাজ করতে পারে।
* ক্যামেরা - এগুলি কোন ছবি বা স্ট্রিমিং ভিডিও গ্রহণ করে বিশ্বের চিত্রিত প্রতিরূপ তৈরী করতে পারে।
* একসেলেরোমিটার - বিভিন্ন দিকে গতিবিধির পরিবর্তন বুঝতে পারে।
* মাইক্রোফোন - সাধারণ শব্দ স্তর বা সুনির্দিষ্ট কোন দিক থেকে আসা শব্দ বুঝতে পারে।
✅ ছোট্ট একটি কাজ করা যাক এখন। আমাদের ব্যবহৃত ফোনে কী কী সেন্সর রয়েছে তা চিন্তা করি।
সব সেন্সর এর মধ্যে একটি সাধারণ বিষয় রয়েছে - তারা যা কিছু সেন্স করতে পারে, তা বৈদ্যুতিক সংকেতে রূপান্তর করে যে ডেটা আইওটি ডিভাইস ব্যবহার করতে পারে। এই বৈদ্যুতিক সংকেতটি কীভাবে ব্যবহৃত হবে, তা সেন্সরের উপর নির্ভর করে, পাশাপাশি আইওটি ডিভাইসের সাথে যোগাযোগ করার জন্য ব্যবহৃত যোগাযোগ প্রোটোকলও এই ক্ষেত্রে প্রভাব রাখে।
## একটি সেন্সর ব্যবহার
আইওটি ডিভাইসে সেন্সর যুক্ত করতে নীচের কোন একটি প্রাসঙ্গিক গাইড অনুসরণ করতে হবে:
* [Arduino - Wio Terminal](wio-terminal-sensor.md)
* [Single-board computer - Raspberry Pi](pi-sensor.md)
* [Single-board computer - Virtual device](virtual-device-sensor.md)
## সেন্সর কত প্রকার
সেন্সরগুলি মূলত ২ প্রকার - অ্যানালগ এবং ডিজিটাল।
### অ্যানালগ সেন্সর
সবথেকে বেসিক সেন্সর হল এনালগ সেন্সর। এগুলো আইওটি ডিভাইস থেকে একটি ভোল্টেজ গ্রহণ করে, সেন্সর উপাদানগুলি এই ভোল্টেজটি সামঞ্জস্য করে এবং সেন্সর থেকে ফিরে আসা ভোল্টেজটিই মান পরিমাপ করে।
> 🎓ভোল্টেজ এমন এক রাশি যা বিদ্যুৎ এক জায়গা থেকে অন্য জায়গায় প্রবাহিত হবে কিনা তা ঠিক করে, যেমন ব্যাটারির পসিটিভ টার্মিনাল থেকে নেগেটিভ টার্মিনালে যায়। উদাহরণস্বরূপ, একটি স্ট্যান্ডার্ড ডাবল-এ ব্যাটারি 1.5V (V হলো ভোল্টের প্রতীক) এবং এটি পসিটিভ টার্মিনাল থেকে নেগেটিভ টার্মিনালে 1.5V এর শক্তি দিয়ে বিদ্যুৎকে প্রবাহিত করতে পারে। বিভিন্ন বৈদ্যুতিক হার্ডওয়্যারের কাজ করার জন্য বিভিন্ন ভোল্টেজের প্রয়োজন হয়, উদাহরণস্বরূপ, একটি এলইডি ২-৩ ভোল্টেজের মধ্যে আলোকিত হতে পারে, তবে একটি 100ওয়াট ফিলামেন্ট লাইটবাল্বের জন্য 240V প্রয়োজন হবে। [ভোল্টেজ - উইকিপিডিয়া পেইজ](https://wikipedia.org/wiki/Voltage) পড়লে এই সংক্রান্ত বিস্তারিত জানা যাবে।
উদাহরণস্বরূপ পোটেনশিওমিটার এর কথা ধরা যাক। এটি এমন একটি ডায়াল যা আমরা দুটি অবস্থানের মধ্যে ঘোরাই এবং সেন্সরটি ঘূর্ণনটি পরিমাপ করে প্রয়োজনীয় তথ্য সংগ্রহ করে।
![A potentiometer set to a mid point being sent 5 volts returning 3.8 volts](../../../images/potentiometer.png)
***পটেনশিওমিটার । Microcontroller by Template / dial by Jamie Dickinson - all from the [Noun Project](https://thenounproject.com)***
আইওটি ডিভাইসগুলো কোন নির্দিষ্ট ভোল্টেজে (যেমনঃ 5V) পোটেনশিওমিটারে বৈদ্যুতিক সংকেত পাঠাবে। পটেনশিওমিটার অ্যাডজাস্ট করার সাথে সাথে এটি অন্য দিক থেকে আগত ভোল্টেজকে পরিবর্তন করে। কল্পনা করি যে ভলিউম নব এর মতো আমাদের ডায়াল হিসাবে 0 থেকে [11] (https://wikedia.org/wiki/Up_to_eleven) লেবেলযুক্ত একটি পটেনশিওমিটার রয়েছে। যখন পেন্টিয়োমিটার পূর্ণ অফ অবস্থানে (0) থাকবে তখন 0V (0 ভোল্ট)আর যখন এটি সম্পূর্ণ অন পজিশনে থাকবে (11), তখন 5V (5 ভোল্ট) মান দিবে।
> 🎓 পুরো বিষয়টিকে অত্যন্ত সহজভাবে বোঝানোর চেষ্টা করা হয়েছে। পোটেনশিওমিটার এবং পরিবর্তনযোগ্য রোধক সম্পর্কে [পোটেনশিওমিটার উইকিপিডিয়া পেইজ](https://wikipedia.org/wiki/Potentiometer) এ বিশদ ব্যখ্যা রয়েছে।
সেন্সর প্রদত্ত ভোল্টেজটি আইওটি ডিভাইস গ্রহণ করে এবং ডিভাইস এটিতে সাড়া দিতে পারে। সেন্সরের উপর নির্ভর করে, এই ভোল্টেজ যেকোন মান এর হতে পারে বা একটি আদর্শ মান ধারণ করতে পারে। উদাহরণস্বরূপ, [থার্মিস্টর] (https://wikedia.org/wiki/Thermistor) এর উপর ভিত্তি করে তৈরী একটি অ্যানালগ তাপমাত্রা সেন্সর, তাপমাত্রার উপর নির্ভর করে তার রেসিস্ট্যান্ট বা রোধ এর মান পরিবর্তন করে। আউটপুট ভোল্টেজটি তখন ক্যালভিন তাপমাত্রায় রূপান্তরিত করা যাবে এবং কোডিং এর মাধ্যমে সেলসিয়াস বা ফারেনহাইটে পরিবর্তনযোগ্য।
✅ সেন্সর যদি প্রদত্ত ভোল্টেজ (যেমনঃ কোন এক্সটার্নাল পাওয়ার সাপ্লাই থেকে) এর চাইতে বেশি রিটার্ন করে , তাহলে কী ঘটবে বলে মনে হয় ? ⛔️ এটার বাস্তবিক টেস্ট করা থেকে সর্বাবস্থায় বিরত থাকা উচিত।
#### অ্যানালগ থেকে ডিজিটালে রূপান্তর
আইওটি ডিভাইসগুলি হলো ডিজিটাল যন্ত্র - এগুলো অ্যানালগ মান নিয়ে কাজ করতে পারে না, তারা কেবল 0 এবং 1 এর মাধ্যমে কাজ করে। এর অর্থ হল এনালগ সেন্সর মানগুলি নিয়ে কাজ করার আগে, তাদেরকে ডিজিটাল সিগন্যালে রূপান্তর করা দরকার। অনেক আইওটি ডিভাইসে এনালগ ইনপুটগুলিকে তাদের মানের ডিজিটাল ফর্মে রূপান্তর করতে 'অ্যানালগ-থেকে-ডিজিটাল কনভার্টার (এডিসি)' থাকে। সেন্সরগুলি সংযোগকারী বোর্ডের মাধ্যমেও এডিসিগুলির সাথে কাজ করতে পারে। উদাহরণস্বরূপ, রাস্পবেরি পাই সহ সীড গ্রোভ ইকোসিস্টেমে, অ্যানালগ সেন্সরগুলি একটি 'হ্যাট'- এর নির্দিষ্ট পোর্টের সাথে সংযোগ করে যা পাই এর জিপিআইও পিনের সাথে যুক্ত হয়ে পাইতে বসে এবং এই হ্যাটটির একটি 'অ্যানালগ-থেকে-ডিজিটাল কনভার্টার (এডিসি)' রয়েছে যা প্রাপ্ত মানকে ডিজিটাল সিগন্যালে পরিণত করে যা জিপিআইও দ্বারা প্রেরিত হয়।
কল্পনা করি যে আমাদের আইওটি ডিভাইসের সাথে সংযুক্ত একটি এনালগ লাইট সেন্সর রয়েছে যা 3.3V ব্যবহার করে এবং 1V রিটার্ন করছে । এই 1V বিষয়টি ডিজিটাল জগতে কোন কিছুই বোঝায়না, তাই এটি রূপান্তর করা দরকার। ভোল্টেজটিকে ডিভাইস এবং সেন্সরের উপর নির্ভর করে, নির্দিষ্ট স্কেলে ব্যবহার করে অ্যানালগ মানে রূপান্তরিত করা হবে। উদাহরণস্বরূপ, সীড গ্রোভ লাইট সেন্সর যা 0 থেকে 1023 পর্যন্ত মান আউটপুট দেয়। 3.3V-তে চলমান এই সেন্সরটির জন্য, 1V আউটপুটটির মান হবে 300 । একটি আইওটি ডিভাইস 300 এনালগ মান হিসাবে পরিচালনা করতে পারে না, সুতরাং মানটি `0000000100101100` এ রূপান্তরিত হবে, যা গ্রোভের দ্বারা রুপান্তরিত 300 এর বাইনারি রূপ এবং এটি পরে আইওটি ডিভাইস দ্বারা প্রক্রিয়া করা হবে।
✅ বাইনারি সম্পর্কে জানা না থাকলে, 0 এবং 1 দ্বারা লিখিত এই সংখ্যাপদ্ধতি সম্পর্কে আমাদের জানতে হবে। এই [BBC Bitesize introduction to binary lesson](https://www.bbc.co.uk/bitesize/guides/zwsbwmn/revision/1) থেকে আমরা আমাদের বাইনারি সংক্রান্ত জ্ঞান আহরণ শুরু করতে পারি।
কোডিং দৃষ্টিকোণ থেকে, এইসব বিষয় সাধারণত সেন্সরগুলির সাথে থাকা লাইব্রেরি দ্বারা পরিচালিত হয়, সুতরাং আমাদেরকে আসলে এই রূপান্তর সম্পর্কে অতোটা চিন্তা করার দরকার নেই। গ্রোভ লাইট সেন্সরের জন্য আমরা পাইথন লাইব্রেরিটি ব্যবহার করবো এবং `light` নামক প্রপার্টিকে কল করলে বা আরডুইনো লাইব্রেরিটি ব্যবহার করে `analogRead` কল করলে, 300 এর মান পাওয়া যাবে।
### ডিজিটাল সেন্সর
অ্যানালগ সেন্সরের মতো ডিজিটাল সেন্সরগুলি বৈদ্যুতিক ভোল্টেজের পরিবর্তনগুলি ব্যবহার করে চারপাশের বাহ্যিক জগৎ সনাক্ত করে। পার্থক্য হল ডিজিটাল সেন্সরগুলো হয় ২টি ভিন্ন স্টেট এর মাঝে তুলনা করে বা বিল্ট-ইন এডিসি ব্যবহার করে একটি ডিজিটাল সিগন্যাল আউটপুট দেয়। বর্তমানে বিভিন্ন কানেক্টর বোর্ড বা আইওটি ডিভাইসে এডিসি এর ব্যবহার এড়ানোর জন্য ডিজিটাল সেন্সরগুলোই বেশি ব্যবহৃত হচ্ছে।
সবচেয়ে সহজ সাধারণ ডিজিটাল সেন্সর হলো বাটন বা স্যুইচ। এটি ২টি অবস্থা সম্পন্ন একটি সেন্সর , অবস্থা দুটি হলো চালু (on) এবং বন্ধ (off) ।
![A button is sent 5 volts. When not pressed it returns 0 volts, when pressed it returns 5 volts](../../../images/button.png)
***বাটন । Microcontroller by Template / Button by Dan Hetteix - all from the [Noun Project](https://thenounproject.com)***
আইওটি ডিভাইসে থাকা পিনগুলি যেমন জিপিআইও পিনগুলি এই সংকেতটি সরাসরি 0 বা 1 হিসাবে পরিমাপ করতে পারে। প্রেরিত এবং প্রাপ্ত ভোল্টেজ সমান হলে, এর মান হয় 1, অন্যথায় মানটি হয় 0। এক্ষেত্রে সিগন্যাল রূপান্তর করার দরকার নেই কারণ এদের মান কেবল 1 বা 0 হতে পারে।
> 💁 ভোল্টেজগুলি কখনই হুবহু মিলেনা না, বিশেষত যেহেতু একটি সেন্সরের উপাদানগুলির রোধ থাকে, তাই এক্ষেত্রে ভোল্টেজের হেরফের হয়। উদাহরণস্বরূপ, জিপিআইও পিনগুলি একটি রাস্পবেরি পাইতে 3.3V-তে কাজ করে এবং রিটার্ন সিগন্যালে 1.8V এর উপর ভোল্টেজ এর মানকে 1 হিসেবে বিবেচনা করে এবং 1.8V এর কম হলে 0 হিসাবে বিবেচনা করে থাকে।
* 3.3V বাটনে যায়। এটি বন্ধ (off), তাই 0V বেরিয়ে আসে, তাই এর মান হয় 0 ।
* 3.3V বাটনে যায়। এটি চালু (on), তাই 3.3V বেরিয়ে আসে,তাই এর মান হয় 1 ।
আরও উন্নত ডিজিটাল সেন্সরগুলো অ্যানালগ মানগুলি গ্রহণ করে, তারপরে অন-বোর্ড এডিসি ব্যবহার করে ডিজিটাল সিগন্যালে রূপান্তর করে। উদাহরণস্বরূপ, একটি ডিজিটাল টেম্পারেচার সেন্সর, এনালগ সেন্সরের মতোই থার্মোকাপল ব্যবহার করবে এবং বর্তমান তাপমাত্রায় থার্মোকাপলের রোধের কারণে সৃষ্ট ভোল্টেজের পরিবর্তনকে পরিমাপ করবে। এনালগ ভ্যালু রিটার্ন করে এটিকে ডিজিটাল সিগন্যালে রূপান্তরের জন্য যন্ত্র বা কানেক্টর বোর্ডের উপর নির্ভর করার পরিবর্তে, সেন্সরের বিল্ট-ইন সেন্সরটিই এই রূপান্তর করে দেয় এবং 0 আর 1 সিরিজবিশিষ্ট মান রিটার্ন করে আইওটি ডিভাইসে। একটি বাটন যেমন 1 বলতে ফুল ভোল্টেজ এবং 0 বলতে শূণ্য ভোল্টেজ বোঝায়, এখানেও একইভাবে সম্পূর্ন বাইনারি সিরিজটি প্রেরিত হয়।
![A digital temperature sensor converting an analog reading to binary data with 0 as 0 volts and 1 as 5 volts before sending it to an IoT device](../../../images/temperature-as-digital.png)
***ডিজিটাল তাপমাত্রা সেন্সর । Temperature by Vectors Market / Microcontroller by Template - all from the [Noun Project](https://thenounproject.com)***
ডিজিটাল ডেটা প্রেরণের জন্য সেন্সরগুলো আরও জটিল হয়ে উঠতে শুরু করেছে। একইসাথে অনেক বেশি বিস্তারিরভাবে তথ্য প্রেরণ করা হচ্ছে, এমনকি সুরক্ষিত সেন্সরগুলির জন্য এনক্রিপ্ট করা ডেটা প্রেরণের ব্যবস্থাও লক্ষ্যণীয়। এর একটি উদাহরণ হলো ক্যামেরা - এটি এমন একটি সেন্সর যা একটি চিত্র ধারণ করে এবং আইওটি ডিভাইস এর জন্য সাধারণত JPEG এর মতো সংকোচিত বিন্যাসে এটি ডিজিটাল ডেটা হিসাবে প্রেরিত হয়। চিত্রধারণ করে, ক্যামেরার পক্ষে ভিডিও স্ট্রীমিংও সম্ভব । হয় পুরো ছবিকে ফ্রেম বাই ফ্রেম সাজিয়ে বা কম্প্রেস করে পাঠানোর মাধ্যমে স্ট্রীমিং হয়ে থাকে।
## অ্যাকচুয়েটর কী?
অ্যাকচুয়েটর হলো সেন্সর এর বিপরীত - এগুলো আইওটি ডিভাইস থেকে বৈদ্যুতিক সংকেতকে বাহ্যিক জগতের সাথে মিথস্ক্রিয়ায় রূপান্তর করে। যেমন, আলোক বা শব্দ নির্গমন করা বা একটি মোটরকে চালানো।
কিছু অতিব্যবহৃত অ্যাকচুয়েটর হিসেবে বলা যায় -
* এলইডি - চালু করলে, এগুলি আলোকিত হয়।
* স্পিকার - একটি সাধারণ buzzer থেকে শুরু করে, অডিও চালাতে সক্ষম এমন যন্ত্রগুলোই স্পিকার। এরা প্রেরিত সিগন্যালের উপর ভিত্তি করে শব্দ তৈরী করে।
* স্টেপার মোটর - এগুলি সংকেতকে একটি সুনির্দিষ্ট পরিমাণ ঘূর্ণনে রূপান্তর করে, যেমন কোন ডায়ালকে 90° কোণে বাঁকানো।
* রিলে - এগুলো এমন সুইচ যা বৈদ্যুতিক সংকেতের সাহায্যে অন/অফ করা যায়। এগুলো আইওটি ডিভাইস থেকে প্রাপ্ত ক্ষুদ্র মানের ভোল্টেজ দ্বারা বৃহত্তর মানের ভোল্টেজ চালু করে।
* স্ক্রিন - এগুলো বেশ জটিল ধরণের অ্যাকচুয়েটর যা একটি পর্দা (display) এর বিভিন্ন অংশে বিভিন্ন তথ্য প্রদর্শন করে। সাধারণ LED display থেকে শুরু করে হাই-রেস্যুলেশন পর্যন্ত প্রদর্শনযোগ্য স্ক্রিন রয়েছে।
✅ ছোট্ট একটি কাজ করা যাক এখন। আমাদের ব্যবহৃত ফোনে কী কী অ্যাকচুয়েটর রয়েছে তা চিন্তা করি।
## একটি অ্যাকচুয়েটর ব্যবহার
আইওটি ডিভাইসে সেন্সর যুক্ত করতে নীচের কোন একটি প্রাসঙ্গিক গাইডটি অনুসরণ করতে হবে। এই ডিভাইসটি সেন্সর নিয়ন্ত্রিত,আর সাহায্যে nightlight এর প্রজেক্টটি করা হবে। এটি সেন্সর দ্বারা পরিবেশে আলোর মাত্রা শনাক্ত করবে, অ্যাকচুয়েটর হিসেবে এলইডি ব্যবহার করবে যেটি (সেন্সর প্রাপ্ত ডাটা অনুসারে) আলোর মাত্রা কম থাকলে, নিজেই জ্বলে উঠবে।
![A flow chart of the assignment showing light levels being read and checked, and the LED begin controlled](../../../images/assignment-1-flow.png)
***A flow chart of the assignment showing light levels being read and checked, and the LED begin controlled. ldr by Eucalyp / LED by abderraouf omara - all from the [Noun Project](https://thenounproject.com)***
* [Arduino - Wio Terminal](wio-terminal-actuator.md)
* [Single-board computer - Raspberry Pi](pi-actuator.md)
* [Single-board computer - Virtual device](virtual-device-actuator.md)
## অ্যাকচুয়েটর কত প্রকার
সেন্সর এর মতো, অ্যাকচুয়েটরও মূলত ২ প্রকার - অ্যানালগ এবং ডিজিটাল।
### অ্যানালগ অ্যাকচুয়েটর
অ্যানালগ অ্যাকচুয়েটর একটি অ্যানালগ সংকেত নিয়ে এটিকে বাহ্যিক জগতের মিথস্ক্রিয়ায় রূপান্তর করে, যেখানে প্রদত্ত ভোল্টেজের ভিত্তিতে মিথস্ক্রিয়া পরিবর্তিত হয়। উদাহরণ হিসেবে, আমাদের বাসাবাড়িতে ব্যবহৃত নিয়ন্ত্রণযোগ্য লাইটের কথা চিন্তা করা যেতে পারে। এটি প্রাপ্ত ভোল্টেজের ভিত্তিতেই নির্ধারিত হয় যে, এই আলোর ঔজ্জ্বল্য কতটা হবে।
![A light dimmed at a low voltage and brighter at a higher voltage](../../../images/dimmable-light.png)
***A light controlled by the voltage output of an IoT device. Idea by Pause08 / Microcontroller by Template - all from the [Noun Project](https://thenounproject.com)***
সেন্সরগুলির মতো, প্রকৃত আইওটি ডিভাইস ডিজিটাল সিগন্যালে কাজ করে, এনালগ এ নয়। একটি এনালগ সিগন্যাল প্রেরণ করার জন্য, আইওটি ডিভাইসটির জন্য ডিজিটাল টু এনালগ কনভার্টার (DAC) দরকার হয়। DAC হয় আইওটি ডিভাইসে সরাসরি, বা কোনও সংযোজক বোর্ডের সাহায্যে যুক্ত করতে হবে। এটি 0 এবং 1 গুলি আইওটি ডিভাইস থেকে অ্যানালগ ভোল্টেজকে রূপান্তর করবে যা অ্যাকচুয়েটর ব্যবহার করতে পারে।
✅ আইওটি ডিভাইসটি যদি অ্যাকচুয়েটর এর সহ্যসীমার বেশি ভোল্টেজ প্রদান করে , তাহলে কী ঘটবে বলে মনে হয় ? ⛔️ এটার বাস্তবিক টেস্ট করা থেকে সর্বাবস্থায় বিরত থাকা উচিত।
#### পালস-উইথ মড্যুলেশন (PWM)
আইওটি ডিভাইস থেকে অ্যানালগ সিগন্যালে রূপান্তর করার জন্য আর একটি বিকল্প হল পালস-উইথ মড্যুলেশন। এর মধ্যে প্রচুর সংক্ষিপ্ত ডিজিটাল পালস সিগন্যাল প্রেরণ করা হয় যা এটি অ্যানালগ সিগন্যাল হিসাবে কাজ করে। উদাহরণস্বরূপ, PWM দ্বারা মোটরের গতি নিয়ন্ত্রণ করা যাবে।
কল্পনা করি যে আমরা 5V পাওয়ার সাপ্লাই দিয়ে, মোটরটি নিয়ন্ত্রণ করছি। ভোল্টেজটি .০২ সেকেন্ডের জন্য high অর্থাৎ 5V রাখার মাধ্যমে, মোটরে একটি সংক্ষিপ্ত পালস প্রেরণ করি। সেই সময়ে মোটরটি একটি পূর্ণ ঘূর্ণনের দশমাংশ বা 36° ঘুরতে পারে। এর পরে লো সিগন্যাল দিয়ে অর্থাৎ 0V প্রেরণ করে, সিগন্যালটি 0.02 সেকেন্ডের জন্য বিরতি দেয়। তারপরে অন-অফ এর প্রতিটি চক্র 0.04s অবধি চলে। তারপরে আবারও পুনরাবৃত্তি করে।
![Pule width modulation rotation of a motor at 150 RPM](../../../images/pwm-motor-150rpm.png)
***PWM rotation of a motor at 150RPM. motor by Bakunetsu Kaito / Microcontroller by Template - all from the [Noun Project](https://thenounproject.com)***
তাহলে প্রতি সেকেন্ডে ২৫টি পালস দেয়া হচ্ছে যেখানে ৫ভোল্টের প্রতি সিগন্যালে .০২ সেকেন্ডে মোটর ঘুরছে আবার ভোল্টের জন্য .০২ সেকেন্ডে মোটর বিরতি নিচ্ছে। প্রতিটি পালস এখানে মোটরকে একটি ঘূর্ণনের দশমাংশে ঘুরায়, যার অর্থ মোটর প্রতি সেকেন্ডে 2.5 ঘূর্ণন সম্পন্ন করে। এখানে ডিজিটাল সিগন্যাল ব্যবহার করে আমরা একটি মোটরকে প্রতি সেকেন্ডে ২.৫টি করে ঘূর্ণন প্রদান করেছি অর্থাৎ ১৫০ আরপিএম বা [revolutions per minute](https://wikipedia.org/wiki/Revolutions_per_minute) এ ঘুরিয়েছি।
```output
25 pulses per second x 0.1 rotations per pulse = 2.5 rotations per second
2.5 rotations per second x 60 seconds in a minute = 150rpm
```
> 🎓 কোন PWM সিগন্যাল যদি অর্ধেক সময় ON থাকে এবং বাকি অর্ধেক সময় OFF থাকে, তবে এই বিষয়টিকে বলা হয় [50% ডিউটি সাইকেল](https://wikipedia.org/wiki/Duty_cycle)। ডিউটি সাইকেল হলো মূলত অন-অফ এই দুই অবস্থার সময়ের দৈর্ঘ্যের তুলনা।
![Pule width modulation rotation of a motor at 75 RPM](../../../images/pwm-motor-75rpm.png)
***PWM rotation of a motor at 75RPM. motor by Bakunetsu Kaito / Microcontroller by Template - all from the [Noun Project](https://thenounproject.com)***
পালসের আকার পরিবর্তন করে মোটরের গতি পরিবর্তন করা যাবে। উদাহরণস্বরূপ, একই মোটর দিয়ে আমরা 0.04 সেকেন্ডের একই চক্র রাখতে পারবো যেখানে ON পালসটি 0.01 ধরে থাকবে এবং OFF পালসটি 0.03 সেকেন্ড সময় ধরে থাকবে। আমাদের প্রতি সেকেন্ডে পালসের সংখ্যার পরিমাণ একই রয়েছে (25) তবে পালসের ON অবস্থার দৈর্ঘ্য এখন অর্ধেক। একটি অর্ধ দৈর্ঘ্যের পালস মোটরটিকে কেবল একটি ঘূর্ণনের এক বিংশতম পর্যন্ত ঘুরতে দেয় এবং 25 পালস দ্বারা প্রতি সেকেন্ডে 1.25টি ঘূর্ণন সম্পন্ন হব অর্থাৎ ৭৫ আরপিএম । ডিজিটাল সিগন্যালের পালসের গতি পরিবর্তন করে এভাবে অ্যানালগ মোটরের গতি অর্ধেকে নামিয়ে ফেলা যাবে।
```output
25 pulses per second x 0.05 rotations per pulse = 1.25 rotations per second
1.25 rotations per second x 60 seconds in a minute = 75rpm
```
✅ কীভাবে মোটর রোটেশন (বিশেষত কম গতিতে) মসৃণ রাখা যায় ? এখানে দীর্ঘ বিরতি সহ স্বল্প সংখ্যক লম্বা পালস নাকি খুব সংক্ষিপ্ত বিরতি দিয়ে প্রচুর সংক্ষিপ্ত পালস - কোনটি ব্যবহার করা উচিত ?
> 💁 কিছু সেন্সরও PWM ব্যবহার করে অ্যানালগ সিগন্যালগুলিকে ডিজিটাল সিগন্যালে রূপান্তর করে।
> 🎓 পালস-উইথ মড্যুলেশন (PWM) এর ব্যপারে আরো জানতে [ঊইকিপিডিয়ার এই আর্টিকেল](https://wikipedia.org/wiki/Pulse-width_modulation) পড়া ভালো হবে।
### ডিজিটাল অ্যাকচুয়েটর
ডিজিটাল অ্যাকচুয়েটরও ডিজিটাল সেন্সরগুলর মতো হয় উচ্চ বা নিম্ন ভোল্টেজের দ্বারা নিয়ন্ত্রিত দুটি স্টেট এ থাকে বা একটি DAC বিল্ট-ইন থাকে, যাতে ডিজিটাল সিগন্যালটিকে এনালগকে রূপান্তর করতে পারে।
একটি সাধারণ ডিজিটাল অ্যাকচুয়েটর এর উদাহরণ হল একটি এলইডি। যখন কোন ডিভাইস ডিজিটাল সিগন্যাল হিসেবে 1 প্রেরণ করে, তখন একটি উচ্চ ভোল্টেজ প্রেরণ করা হয় যা LED জ্বালায় । আবার 0 এর একটি ডিজিটাল সিগন্যাল প্রেরণ করা হলে, ভোল্টেজ 0V এ নেমে আসে এবং LED বন্ধ হয়ে যায়।
![A LED is off at 0 volts and on at 5V](../../../images/led.png)
***An LED turning on and off depending on voltage. LED by abderraouf omara / Microcontroller by Template - all from the [Noun Project](https://thenounproject.com)***
✅ ২-অবস্থা বিশিষ্ট আর কোন অ্যাকচুয়েটর কী আশেপাশে দেখা যায় ? একটি উদাহরণ হলো সলিনয়েড, একটি ইলেক্ট্রোম্যাগনেট যা দ্বারা কোন দরজার নব নিয়ন্ত্রণ করে খোলা-বন্ধ করা যাবে।
আরও উন্নত ডিজিটাল অ্যাকচুয়েটর যেমন স্ক্রিনের জন্য ডিজিটাল ডেটা নির্দিষ্ট ফর্ম্যাটে প্রেরণ করা প্রয়োজন। এগুলি সাধারণত প্রোগ্রাম লাইব্রেরিতে থাকে যা এগুলি নিয়ন্ত্রণ করতে সঠিক ডেটা প্রেরণকে সহজ করে।
---
## 🚀 চ্যালেঞ্জ
শেষ দুটি পাঠের চ্যালেঞ্জ ছিল বাসস্থান, স্কুল বা কর্মক্ষেত্রে যতগুলি আইওটি ডিভাইস রয়েছে তা তালিকাভুক্ত করা এবং তারা মাইক্রোকন্ট্রোলার বা একক-বোর্ড কম্পিউটার, বা উভয়ের মিশ্রণের দ্বারা নির্মিত কিনা তা সিদ্ধান্ত নেওয়া। এবারের চ্যালেঞ্জ হলো, তালিকাভুক্ত প্রতিটি ডিভাইসের জন্য, তারা কোন সেন্সর এবং অ্যাকচুয়েটর সাথে সংযুক্ত আছে? এই ডিভাইসগুলির সাথে সংযুক্ত প্রতিটি সেন্সর এবং অ্যাকচুয়েটরের উদ্দেশ্য কী?
## লেকচার পরবর্তী কুইজ
[লেকচার পরবর্তী কুইজ](https://brave-island-0b7c7f50f.azurestaticapps.net/quiz/6)
## রিভিউ এবং স্ব-অধ্যয়ন
* [ThingLearn](http://www.thinglearn.com/essentials/) থেকে ইলেক্ট্রিসিটি ও সার্কিটের ব্যাপারে পড়া।
* [Seeed Studios Temperature Sensors guide](https://www.seeedstudio.com/blog/2019/10/14/temperature-sensors-for-arduino-projects/) থেকে বিভিন্ন ধরণের তাপমাত্রা সেন্সরের ব্যাপারে জানা।
* এলইডি সম্পর্কে [Wikipedia LED page](https://wikipedia.org/wiki/Light-emitting_diode) থেকে আরো বিস্তারিত ধারণা লাভ করা।
## এসাইনমেন্ট
[সেন্সর এবং অ্যাকচুয়েটর নিয়ে গবেষণা ](assignment.md)

@ -0,0 +1,17 @@
# সেন্সর এবং অ্যাকচুয়েটর সংক্রান্ত গবেষণা
## নির্দেশনা
এই পাঠটিতে সেন্সর এবং অ্যাকচুয়েটর আলোচনা হয়েছে। একটি আইওটি ডেভলাপার কিটে ব্যবহার করা যেতে পারে এমন একটি সেন্সর এবং একটি অ্যাকচুয়েটর বর্ণনা করতে হবে, যেখানে উল্লেখ থাকবে:
* এটি কী কাজ করে
* ভিতরে ব্যবহৃত ইলেকট্রনিক্স/হার্ডওয়্যার
* এটি কি অ্যানালগ নাকি ডিজিটাল
* ইনপুট বা পরিমাপের একক কী এবং যন্ত্রটির ব্যবহার্য সীমা (range) কতটুকু
## এসাইনমেন্ট মূল্যায়ন মানদন্ড
| ক্রাইটেরিয়া | দৃষ্টান্তমূলক ব্যখ্যা (সর্বোত্তম) | পর্যাপ্ত ব্যখ্যা (মাঝারি) | আরো উন্নতির প্রয়োজন (নিম্ন) |
| -------- | --------- | -------- | ----------------- |
| একটি সেন্সর সংক্রান্ত বর্ণনা | উপরে তালিকাভুক্ত 4 টি বিভাগের বিশদ ব্যখ্যা সহ সেন্সর বর্ণিত হয়েছে | একটি সেন্সর বর্ণিত হয়েছ, তবে উপরের তালিকা থেকে কেবল 2-3টি বিষয় ব্যখ্যা করতে সক্ষম হয়েছে | একটি সেন্সর বর্ণিত হয়েছ, তবে উপরের তালিকা থেকে কেবল 1টি বিষয় ব্যখ্যা করতে সক্ষম হয়েছে |
| একটি অ্যাকচুয়েটর সংক্রান্ত বর্ণনা | উপরে তালিকাভুক্ত 4 টি বিভাগের বিশদ ব্যখ্যা সহ অ্যাকচুয়েটর বর্ণিত হয়েছে | একটি অ্যাকচুয়েটর বর্ণিত হয়েছ, তবে উপরের তালিকা থেকে কেবল 2-3টি বিষয় ব্যখ্যা করতে সক্ষম হয়েছে | একটি অ্যাকচুয়েটর বর্ণিত হয়েছ, তবে উপরের তালিকা থেকে কেবল 1টি বিষয় ব্যখ্যা করতে সক্ষম হয়েছে |

@ -1,8 +1,8 @@
# Connect your device to the Internet
Add a sketchnote if possible/appropriate
![A sketchnote overview of this lesson](../../../sketchnotes/lesson-4.png)
![Embed a video here if available](video-url)
> Sketchnote by [Nitya Narasimhan](https://github.com/nitya). Click the image for a larger version.
## Pre-lecture quiz

@ -384,6 +384,8 @@ For now, you won't be updating your server code. Instead you can use the Azure C
The time values in the annotations are in [UNIX time](https://wikipedia.org/wiki/Unix_time), representing the number of seconds since midnight on 1<sup>st</sup> January 1970.
Exit the event monitor when you are done.
### Task - control your IoT device
You can also use the Azure CLI to call direct methods on your IoT device.

@ -311,7 +311,7 @@ This will create a folder inside the `soil-moisture-trigger` folder called `iot-
* `"type": "eventHubTrigger"` - this tells the function it needs to listen to events from an Event Hub
* `"name": "events"` - this is the parameter name to use for the Event Hub events. This matches the parameter name in the `main` function in the Python code.
* `"direction": "in",` - this is an input binding, the data from the event hub comes into the function
* `"direction": "in"` - this is an input binding, the data from the event hub comes into the function
* `"connection": ""` - this defines the name of the setting to read the connection string from. When running locally, this will read this setting from the `local.settings.json` file.
> 💁 The connection string cannot be stored in the `function.json` file, it has to be read from the settings. This is to stop you accidentally exposing your connection string.
@ -326,6 +326,10 @@ This will create a folder inside the `soil-moisture-trigger` folder called `iot-
### Task - run the event trigger
1. Make sure you are not running the IoT Hub event monitor. If this is running at the same time as the functions app, the functions app will not be able to connect and consume events.
> 💁 Multiple apps can connect to the IoT Hub endpoints using different *consumer groups*. These are covered in a later lesson.
1. To run the Functions app, run the following command from the VS Code terminal
```sh

@ -146,6 +146,8 @@ After the connection, all data sent to the IoT Hub from the device, or to the de
>
> When learning IoT it is often easier to put the key in code, as you did in an earlier lesson, but you must ensure this key is not checked into public source code control.
Devices have 2 keys, and 2 corresponding connection strings. This allows you to rotate the keys - that is switch from one key to another if the first gets compromised, and re-generate the first key.
### X.509 certificates
When you are using asymmetric encryption with a public/private key pair, you need to provide your public key to anyone who wants to send you data. The problem is, how can the recipient of your key be sure it's actually your public key, not someone else pretending to be you? Instead of providing a key, you can instead provide your public key inside a certificate that has been verified by a trusted third party, called an X.509 certificate.

@ -138,7 +138,7 @@ You can use a GPS sensor on your IoT device to get GPS data.
### Task - connect a GPS sensor and read GPS data
Work through the relevant guide to measure soil moisture using your IoT device:
Work through the relevant guide to read GPS data using your IoT device:
* [Arduino - Wio Terminal](wio-terminal-gps-sensor.md)
* [Single-board computer - Raspberry Pi](pi-gps-sensor.md)

@ -10,7 +10,7 @@ Add a sketchnote if possible/appropriate
## Introduction
In the last lesson, you learned how to use a GPS sensor to capture location data. To use this data to visualize the both the location of a truck laden with food, but also it's journey, it needs to be sent to an IoT service in the cloud, and then stored somewhere.
In the last lesson, you learned how to use a GPS sensor to capture location data. To use this data to visualize the location of a truck laden with food, and it's journey, it needs to be sent to an IoT service in the cloud, and then stored somewhere.
In this lesson you will learn about the different ways to store IoT data, and learn how to store data from your IoT service using serverless code.
@ -64,7 +64,7 @@ SQL databases are ideal for storing structured data, and for when you want to en
#### NoSQL database
NoSQL databases are so called because they don't have the same rigid structure of SQL databases. There are also known as document databases as they can store unstructured data such as documents.
NoSQL databases are called NoSQL because they don't have the same rigid structure of SQL databases. They are also known as document databases as they can store unstructured data such as documents.
> 💁 Despite their name, some NoSQL databases allow you to use SQL to query the data.
@ -88,7 +88,7 @@ In the last lesson you captured GPS data from a GPS sensor connected to your IoT
1. Create a new IoT Hub using the free tier.
> ⚠️ You can refer to [the instructions for creating an IoT Hub from project 2, lesson 4 if needed](../../../2-farm/lessons/4-migrate-your-plant-to-the-cloud/README.md#create-an-iot-service-in-the-cloud).
> ⚠️ You can refer to the [instructions for creating an IoT Hub from project 2, lesson 4](../../../2-farm/lessons/4-migrate-your-plant-to-the-cloud/README.md#create-an-iot-service-in-the-cloud) if needed.
Remember to create a new Resource Group. Name the new Resource Group `gps-sensor`, and the new IoT Hub a unique name based on `gps-sensor`, such as `gps-sensor-<your name>`.
@ -98,7 +98,7 @@ In the last lesson you captured GPS data from a GPS sensor connected to your IoT
1. Update your device code to send the GPS data to the new IoT Hub using the device connection string from the previous step.
> ⚠️ You can refer to [the instructions for connecting your device to an IoT from project 2, lesson 4 if needed](../../../2-farm/lessons/4-migrate-your-plant-to-the-cloud/README.md#connect-your-device-to-the-iot-service).
> ⚠️ You can refer to the [instructions for connecting your device to an IoT from project 2, lesson 4](../../../2-farm/lessons/4-migrate-your-plant-to-the-cloud/README.md#connect-your-device-to-the-iot-service) if needed.
1. When you send the GPS data, do it as JSON in the following format:
@ -148,11 +148,11 @@ Once data is flowing into your IoT Hub, you can write some serverless code to li
1. Create an Azure Functions app using the Azure Functions CLI. Use the Python runtime, and create it in a folder called `gps-trigger`, and use the same name for the Functions App project name. Make sure you create a virtual environment to use for this.
> ⚠️ You can refer to [the instructions for creating an Azure Functions Project from project 2, lesson 5 if needed](../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#create-a-serverless-application).
> ⚠️ You can refer to the [instructions for creating an Azure Functions Project from project 2, lesson 5](../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#create-a-serverless-application) if needed.
1. Add an IoT Hub event trigger that uses the IoT Hub's Event Hub compatible endpoint.
> ⚠️ You can refer to [the instructions for creating an IoT Hub event trigger from project 2, lesson 5 if needed](../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#create-an-iot-hub-event-trigger).
> ⚠️ You can refer to the [instructions for creating an IoT Hub event trigger from project 2, lesson 5](../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#create-an-iot-hub-event-trigger) if needed.
1. Set the Event Hub compatible endpoint connection string in the `local.settings.json` file, and use the key for that entry in the `function.json` file.
@ -180,13 +180,13 @@ You will use blob storage in this lesson to store IoT data.
### Table storage
Table storage allows you to store semi-structured data. Table storage is actually a NoSQL database, so doesn't require a defined set of tables up front, but is designed to store data in one or more tables, with unique keys to define each row.
Table storage allows you to store semi-structured data. Table storage is actually a NoSQL database, so doesn't require a defined set of tables up front, but it is designed to store data in one or more tables, with unique keys to define each row.
✅ Do some research: Read up on [Azure Table Storage](https://docs.microsoft.com/azure/storage/tables/table-storage-overview?WT.mc_id=academic-17441-jabenn)
### Queue storage
Queue storage allows you to store messages of up to 64KB in size in a queue. You can add messages to the back of the queue, and read them off the front. Queues store messages indefinitely as long as there is still storage space, so allows messages to be stored long term. then read off when needed. For example, if you wanted to run a monthly job to process GPS data you could add it to a queue every day for a month, then at the end of the month process all the messages off the queue.
Queue storage allows you to store messages of up to 64KB in size in a queue. You can add messages to the back of the queue, and read them off the front. Queues store messages indefinitely as long as there is still storage space, so it allows messages to be stored long term. then read off when needed. For example, if you wanted to run a monthly job to process GPS data you could add it to a queue every day for a month, then at the end of the month process all the messages off the queue.
✅ Do some research: Read up on [Azure Queue Storage](https://docs.microsoft.com/azure/storage/queues/storage-queues-introduction?WT.mc_id=academic-17441-jabenn)
@ -227,7 +227,7 @@ The data will be saved as a JSON blob with the following format:
1. Create an Azure Storage account. Name it something like `gps<your name>`.
> ⚠️ You can refer to [the instructions for creating a storage account from project 2, lesson 5 if needed](../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#task---create-the-cloud-resources).
> ⚠️ You can refer to the [instructions for creating a storage account from project 2, lesson 5](../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#task---create-the-cloud-resources) if needed.
If you still have a storage account from the previous project, you can re-use this.
@ -341,6 +341,9 @@ The data will be saved as a JSON blob with the following format:
[2021-05-21T01:31:14.351Z] Writing blob to gps-sensor/4b6089fe-ba8d-11eb-bc7b-1e00621e3648.json - {'device_id': 'gps-sensor', 'timestamp': '2021-05-21T00:57:53.878Z', 'gps': {'lat': 47.73092, 'lon': -122.26206}}
```
> 💁 Make sure you are not running the IoT Hub event monitor at the same time.
> 💁 You can find this code in the [code/functions](code/functions) folder.
### Task - verify the uploaded blobs
@ -406,15 +409,15 @@ Now that your Function app is working, you can deploy it to the cloud.
1. Create a new Azure Functions app, using the storage account you created earlier. Name this something like `gps-sensor-` and add a unique identifier on the end, like some random words or your name.
> ⚠️ You can refer to [the instructions for creating a Functions app from project 2, lesson 5 if needed](../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#task---create-the-cloud-resources).
> ⚠️ You can refer to the [instructions for creating a Functions app from project 2, lesson 5](../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#task---create-the-cloud-resources) if needed.
1. Upload the `IOT_HUB_CONNECTION_STRING` and `STORAGE_CONNECTION_STRING` values to the Application Settings
> ⚠️ You can refer to [the instructions for uploading Application Settings from project 2, lesson 5 if needed](../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#task---upload-your-application-settings).
> ⚠️ You can refer to the [instructions for uploading Application Settings from project 2, lesson 5](../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#task---upload-your-application-settings) if needed.
1. Deploy your local Functions app to the cloud.
> ⚠️ You can refer to [the instructions for deploying your Functions app from project 2, lesson 5 if needed](../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#task---deploy-your-functions-app-to-the-cloud).
> ⚠️ You can refer to the [instructions for deploying your Functions app from project 2, lesson 5](../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#task---deploy-your-functions-app-to-the-cloud) if needed.
---

@ -1,139 +1,199 @@
# Visualize location data
Add a sketchnote if possible/appropriate
This video gives an overview of OAzure Maps with IoT, a service that will be covered in this lesson.
[![Azure Maps - The Microsoft Azure Enterprise Location Platform](https://img.youtube.com/vi/P5i2GFTtb2s/0.jpg)](https://www.youtube.com/watch?v=P5i2GFTtb2s)
> 🎥 Click the image above to watch the video
## Pre-lecture quiz
[Pre-lecture quiz](https://brave-island-0b7c7f50f.azurestaticapps.net/quiz/25)
## Introduction
In the last lesson you learned how to get GPS data from your sensors to save to the cloud in a storage container using serverless code. Now you will discover how to visualize those points on an Azure map.
In the last lesson you learned how to get GPS data from your sensors to save to the cloud in a storage container using serverless code. Now you will discover how to visualize those points on an Azure map. You will learn how to create a map on a web page, learn about the GeoJSON data format and how to use it to plot all the captured GPS points on your map.
In this lesson we'll cover:
- [What is Azure maps?](#what-is-azure-maps)
- [Create an Azure Maps resource](#create-an-azure-maps-resource)
- [Show a map on a web page](#show-a-map-on-a-web-page)
- [The GeoJSON format](#the-geojson-format)
- [Plot GPS data on a Map using GeoJSON](#plot-gps-data-on-a-map-using-geojson)
* [What is data visualization](#what-is-data-visualization)
* [Map services](#map-services)
* [Create an Azure Maps resource](#create-an-azure-maps-resource)
* [Show a map on a web page](#show-a-map-on-a-web-page)
* [The GeoJSON format](#the-geojson-format)
* [Plot GPS data on a Map using GeoJSON](#plot-gps-data-on-a-map-using-geojson)
> 💁 This lesson will involve a small amount of HTML and JavaScript. If you would like to learn more about web development using HTML and JavaScript, check out [Web development for beginners](https://github.com/microsoft/Web-Dev-For-Beginners).
## What is data visualization
Data visualization, as the name suggests, is about visualizing data in ways that make it easier for humans to understand. It is usually associated with charts and graphs, but is any way of pictorially representing data to help humans to not only understand the data better, but help them make decisions.
Taking a simple example - back in the farm project you captured soil moisture settings. A table of soil moisture data captured every hour for the 1st June 2021 might be something like the following:
| Date | Reading |
| ---------------- | ------: |
| 01/06/2021 00:00 | 257 |
| 01/06/2021 01:00 | 268 |
| 01/06/2021 02:00 | 295 |
| 01/06/2021 03:00 | 305 |
| 01/06/2021 04:00 | 325 |
| 01/06/2021 05:00 | 359 |
| 01/06/2021 06:00 | 398 |
| 01/06/2021 07:00 | 410 |
| 01/06/2021 08:00 | 429 |
| 01/06/2021 09:00 | 451 |
| 01/06/2021 10:00 | 460 |
| 01/06/2021 11:00 | 452 |
| 01/06/2021 12:00 | 420 |
| 01/06/2021 13:00 | 408 |
| 01/06/2021 14:00 | 431 |
| 01/06/2021 15:00 | 462 |
| 01/06/2021 16:00 | 432 |
| 01/06/2021 17:00 | 402 |
| 01/06/2021 18:00 | 387 |
| 01/06/2021 19:00 | 360 |
| 01/06/2021 20:00 | 358 |
| 01/06/2021 21:00 | 354 |
| 01/06/2021 22:00 | 356 |
| 01/06/2021 23:00 | 362 |
As a human, understanding that data can be hard. It's a wall of numbers without any meaning. As a first step to visualizing this data, it can be plotted on a line chart:
![A line chart of the above data](../../../images/chart-soil-moisture.png)
This can be further enhanced by adding a line to indicate when the automated watering system was turned on at a soil moisture reading of 450:
![A line chart of soil moisture with a line at 450](../../../images/chart-soil-moisture-relay.png)
This chart shows very quickly not only what the soil moisture levels were, but the points where the watering system was turned on.
Charts are not the only tool to visualize data. IoT devices that track weather can have web apps or mobile apps that visualize weather conditions using symbols, such as a cloud symbol for cloudy days, a rain cloud for rainy days and so on. There are a huge number of ways to visualize data, many serious, some fun.
## What is Azure maps?
✅ Think about ways you've seen data visualized. Which methods have been the clearest and have allowed you to make decisions fastest?
Working with maps is an interesting exercise, and there are many to choose from such as Bing Maps, Leaflet, Open Street Maps, and Google Maps. In this lesson, you will learn about [Azure Maps](https://azure.microsoft.com/services/azure-maps/#azuremaps-overview?WT.mc_id=academic-17441-jabenn) and how they can display your GPS data.
The best visualizations allow humans to humans to make decisions quickly. For example, having a wall of gauges showing all manner of readings from industrial machinery is hard to process, but a flashing red light when something goes wrong allows a human to make a decision. Sometimes the best visualization is a flashing light!
✅ Check out [this video](https://sec.ch9.ms/ch9/d498/3d435d2c-ac85-421b-b3a7-5e0c7630d498/IoT_AzureMaps_high.mp4) on using Azure Maps with IoT.
When working with GPS data, the clearest visualization can be to plot the data on a map. A map showing delivery trucks for example, can help workers at a processing plant see when trucks will arrive. If this map shows more that just pictures of trucks at their current locations, but gives an idea of the contents of a truck, then the workers at the plant can plan accordingly - if they see a refrigerated truck close by they know to prepare space in a fridge.
## Map services
Working with maps is an interesting exercise, and there are many to choose from such as Bing Maps, Leaflet, Open Street Maps, and Google Maps. In this lesson, you will learn about [Azure Maps](https://azure.microsoft.com/services/azure-maps/?WT.mc_id=academic-17441-jabenn) and how they can display your GPS data.
![The Azure Maps logo](../../../images/azure-maps-logo.png)
Azure Maps is "a collection of geospatial services and SDKs that use fresh mapping data to provide geographic context to web and mobile applications." Developers are provided with tools to create beautiful, interactive maps that can do things like provide recommended traffic routes, give information about traffic incidents, indoor navigation, search capabilities, elevation information, weather services and more.
> ✅ Experiment with some [mapping code samples](https://docs.microsoft.com/samples/browse/?products=azure-maps?WT.mc_id=academic-17441-jabenn)
✅ Experiment with some [mapping code samples](https://docs.microsoft.com/samples/browse/?products=azure-maps&WT.mc_id=academic-17441-jabenn)
You can display the maps as a blank canvas, tiles, satellite images, satellite images with roads superimposed, various types of grayscale maps, maps with shaded relief to show elevation, night view maps, and a high contrast map. You can get real-time updates on your maps by integrating them with [Azure Event Grid](https://azure.microsoft.com/services/event-grid/?WT.mc_id=academic-17441-jabenn). You can control the behavior and look of your maps by enabling various controls to allow the map to react to events like pinch, drag, and click. To control the look of your map, you can add layers that include bubbles, lines, polygons, heat maps, and more. Which style of map you implement depends on your choice of SDK.
You can access Azure Maps APIs by leveraging its [REST API](https://docs.microsoft.com/javascript/api/azure-maps-rest/?view=azure-maps-typescript-latest?WT.mc_id=academic-17441-jabenn), its [Web SDK](https://docs.microsoft.com/azure/azure-maps/how-to-use-map-control?WT.mc_id=academic-17441-jabenn), or, if you are building a mobile app, its [Android SDK](https://docs.microsoft.com/azure/azure-maps/how-to-use-android-map-control-library?pivots=programming-language-java-android?WT.mc_id=academic-17441-jabenn).
You can access Azure Maps APIs by leveraging its [REST API](https://docs.microsoft.com/javascript/api/azure-maps-rest/?view=azure-maps-typescript-latest&WT.mc_id=academic-17441-jabenn), its [Web SDK](https://docs.microsoft.com/azure/azure-maps/how-to-use-map-control?WT.mc_id=academic-17441-jabenn), or, if you are building a mobile app, its [Android SDK](https://docs.microsoft.com/azure/azure-maps/how-to-use-android-map-control-library?pivots=programming-language-java-android&WT.mc_id=academic-17441-jabenn).
In this lesson, you will use the web SDK to draw a map and display your sensor's GPS location's path.
## Create an Azure Maps resource
Your first step is to create an Azure Maps account. You can do this using the CLI or in the [Azure portal](https://portal.azure.com?WT.mc_id=academic-17441-jabenn).
Your first step is to create an Azure Maps account.
Using the CLI, create a maps account:
### Task - create an Azure Maps resource
```
az maps account create --name
--resource-group
[--accept-tos]
[--sku {S0, S1}]
[--subscription]
[--tags]
```
1. Run the following command from your Terminal or Command Prompt to create an Azure Maps resource in your `gps-sensor` resource group:
Use the `gps-service` resource group you've used in previous lessons. You can use the S0 subscription for this small task.
```sh
az maps account create --name gps-sensor \
--resource-group gps-sensor \
--accept-tos \
--sku S1
```
A sample call would look like this:
This will create an Azure Maps resource called `gps-sensor`. The tier being used is `S1`, which is a paid tier that includes a range of features, but with a generous amount of calls for free.
```
az maps account create --name MyMapsAccount --resource-group MyResourceGroup --sku S0 --subscription MySubscription
```
The service will deploy. Next, you need to get your Subscription Key. There are two ways to authenticate your maps in a web app: using Active Directory (AD) or 'Shared Key Authentication', also known as Subscription Key. We'll use the latter, for simplicity.
> 💁 To see the cost of using Azure Maps, check out the [Azure Maps pricing page](https://azure.microsoft.com/pricing/details/azure-maps/?WT.mc_id=academic-17441-jabenn).
In the CLI, find your keys:
1. You will need an API key for the maps resource. Use the following command to get this key:
```
az maps account keys list --name
--resource-group
[--query-examples]
[--subscription]
```
A sample call would look like this:
```sh
az maps account keys list --name gps-sensor \
--resource-group gps-sensor \
--output table
```
```
az maps account keys list --name MyMapsAccount --resource-group MyResourceGroup
```
✅ You will be able to rotate and swap keys at will using the Shared Keys; switch your app to use the Secondary Key while rotating the Primary Key if needed.
Take a copy of the `PrimaryKey` value.
## Show a map on a web page
Now you can take the next step which is to display your map on a web page. We will use just one .html file for your small web app; keep in mind that in a production or team environment, your web app will most likely have more moving parts!
Now you can take the next step which is to display your map on a web page. We will use just one `html` file for your small web app; keep in mind that in a production or team environment, your web app will most likely have more moving parts!
### Task - show a map on a web page
1. Create a file called index.html in a folder somewhere on your local computer. Add HTML markup to hold a map:
```html
<html>
<head>
```html
<html>
<head>
<style>
#myMap {
width:100%;
height:100%;
}
</style>
</head>
</head>
<body onload="init()">
<body onload="init()">
<div id="myMap"></div>
</body>
</html>
```
</body>
</html>
```
The map will load in the 'myMap' `div`. A few styles allow it so span the width and height of the page.
The map will load in the `myMap` `div`. A few styles allow it to span the width and height of the page.
2. Under the opening `<head>` tag, add an external style sheet to control the map display, and an external script from the Web SDK to manage its behavior:
> 🎓 a `div` is a section of a web page that can be named and styled.
```
<link rel="stylesheet" href="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.css" type="text/css" />
<script src="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.js"></script>
```
1. Under the opening `<head>` tag, add an external style sheet to control the map display, and an external script from the Web SDK to manage its behavior:
3. Under that script, add a script block to launch the map. Add your own subscriptionKey in the init() function:
```html
<link rel="stylesheet" href="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.css" type="text/css" />
<script src="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.js"></script>
```
```javascript
<script type='text/javascript'>
This style sheet contains the settings for how the map looks, and the script file contains code to load the map. Adding this code is similar to including C++ header files or importing Python modules.
1. Under that script, add a script block to launch the map.
```javascript
<script type='text/javascript'>
function init() {
var map = new atlas.Map('myMap', {
center: [-122.26473, 47.73444],
zoom: 12,
authOptions: {
authType: "subscriptionKey",
subscriptionKey: "<your-key-here>",
subscriptionKey: "<subscription_key>",
}
});
}
</script>
```
If you open your index.html page in a web browser, you should see a map loaded, and focused on the Seattle area.
```
Replace `<subscription_key>` with the API key for your Azure Maps account.
If you open your `index.html` page in a web browser, you should see a map loaded, and focused on the Seattle area.
![map image](images/map-image.png)
![A map showing Seattle, a city in Washington State, USA](../../../images/map-image.png)
✅ Experiment with the zoom and center parameters to change your map display. You can add different coordinates corresponding to your data's latitude and longitude to re-center the map.
✅ Experiment with the zoom and center parameters to change your map display. You can add different coordinates corresponding to your data's latitude and longitude to re-center the map.
> 💁 A better way to work with web apps locally is to install [http-server](https://www.npmjs.com/package/http-server). You will need [node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed before using this tool. Once those tools are installed, you can navigate to the location of your `index.html` file and type `http-server`. The web app will open on a local webserver [http://127.0.0.1:8080/](http://127.0.0.1:8080/).
> A better way to work with web apps locally is to install [http-server](https://www.npmjs.com/package/http-server). You will need [node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed before using this tool. Once those tools are installed, you can navigate to the location of your `index.html` file and type `http-server`. The web app will open on a local webserver http://127.0.0.1:8080/.
## The GeoJSON format
Now that you have your web app in place with the map displaying, you need to extract GPS data from your storage and display it in a layer of markers on top of the map. Before we do that, let's look at the [GeoJSON](https://wikipedia.org/wiki/GeoJSON) format that is required by Azure Maps.
Now that you have your web app in place with the map displaying, you need to extract GPS data from your storage account and display it in a layer of markers on top of the map. Before we do that, let's look at the [GeoJSON](https://wikipedia.org/wiki/GeoJSON) format that is required by Azure Maps.
[GeoJSON](https://geojson.org/) is an open standard JSON specification with special formatting designed to handle geographic-specific data. You can learn about it by testing sample data using [geojson.io](geojson.io), which is also a useful tool to debug GeoJSON files.
[GeoJSON](https://geojson.org/) is an open standard JSON specification with special formatting designed to handle geographic-specific data. You can learn about it by testing sample data using [geojson.io](https://geojson.io), which is also a useful tool to debug GeoJSON files.
Sample GeoJSON data looks like this:
@ -155,47 +215,44 @@ Sample GeoJSON data looks like this:
}
```
Of particular interest is the way the data is nested as a 'Feature' within a 'FeatureCollection'. Within that object can be found 'geometry' with the 'coordinates' indicating latitude and longitude.
Of particular interest is the way the data is nested as a `Feature` within a `FeatureCollection`. Within that object can be found `geometry` with the `coordinates` indicating latitude and longitude.
✅ When building your geoJSON, pay attention to the order of 'latitude' and 'longitude' in the object, or your points will not appear where they should!
✅ When building your geoJSON, pay attention to the order of `latitude` and `longitude` in the object, or your points will not appear where they should! GeoJSON expects data in the order `lon,lat` for points, not `lat,lon`.
`Geometry` can have different 'types' designated to that a polygon could be drawn to a map; in this case, a point is drawn with two coordinates designated.
`Geometry` can have different types, such as a single point or a polygon. In this example, it is a point with two coordinates specified, the longitude, and the latitude.
✅ Azure Maps supports standard GeoJSON plus some [enhanced features](https://docs.microsoft.com/azure/azure-maps/extend-geojson?WT.mc_id=academic-17441-jabenn) including the ability to draw circles and other geometries.
## Plot GPS data on a Map using GeoJSON
Now you are ready to consume data from the storage that you built in the previous lesson. As a reminder, it is stored as a number of files in blob storage so you will need to retrieve the files and parse them so that Azure Maps can use the data.
If you make a call to your storage to fetch the data you might be surprised to see errors occurring in your browser's console. That's because you need to set permissions for [CORS](https://developer.mozilla.org/docs/Web/HTTP/CORS) on this storage to allow external web apps to read its data. CORS stands for "Cross-Origin Resource Sharing" and usually needs to be set explicitly in Azure for security reasons. Do this using the Azure CLI, adding the name of your storage container and its key. We only need to 'GET' data from this container:
### Task - configure storage to be accessed from a web page
If you make a call to your storage to fetch the data you might be surprised to see errors occurring in your browser's console. That's because you need to set permissions for [CORS](https://developer.mozilla.org/docs/Web/HTTP/CORS) on this storage to allow external web apps to read its data.
> 🎓 CORS stands for "Cross-Origin Resource Sharing" and usually needs to be set explicitly in Azure for security reasons. It stops sites you don't expect from being able to access your data.
```dotnetcli
az storage cors add --methods GET \
1. Run the following command to enable CORS:
```sh
az storage cors add --methods GET \
--origins "*" \
--services b \
--account-name <storage_name> \
--account-key <key1>
```
```
1. First, get the endpoint of your storage container. Using the Azure CLI, you can show its information:
Replace `<storage_name>` with the name of your storage account. Replace `<key1>` with the account key for your storage account.
```
az storage account blob-service-properties show --account-name
[--query-examples]
[--resource-group]
[--subscription]
```
A typical query would look like:
This command allows any website (the wildcard `*` means any) to make a *GET* request, that is get data, from your storage account. The `--services b` means only apply this setting for blobs.
```
az storage account blob-service-properties show -n mystorageaccount -g MyResourceGroup
```
### Task - load the GPS data from storage
1. Use that endpoint to build up your init() function. Overwrite the previous function by adding the ability to fetch data:
1. Replace the entire contents of the `init` function with the following code:
```javascript
fetch("https://<your-storage-name>.blob.core.windows.net/gps-data/?restype=container&comp=list")
```javascript
fetch("https://<storage_name>.blob.core.windows.net/gps-data/?restype=container&comp=list")
.then(response => response.text())
.then(str => new window.DOMParser().parseFromString(str, "text/xml"))
.then(xml => {
@ -210,7 +267,7 @@ az storage account blob-service-properties show -n mystorageaccount -g MyResourc
zoom: 14,
authOptions: {
authType: "subscriptionKey",
subscriptionKey: "<your-key>",
subscriptionKey: "<subscription_key>",
}
});
@ -221,14 +278,22 @@ az storage account blob-service-properties show -n mystorageaccount -g MyResourc
source.add(features);
})
})
```
```
Replace `<storage_name>` with the name of your storage account. Replace `<subscription_key>` with the API key for your Azure Maps account.
There are several things happening here. First, you fetch your data from your container using the endpoint you found using the Azure CLI. You parse each file in that blog storage to extract latitude and longitude. Then you initialize a map, adding a bubble layer with the data fetched and saved as source.
There are several things happening here. First, the code fetches your GPS data from your blob container using a URL endpoint built using your storage account name. This URL retrieves from `gps-data`, indicating the resource type is a container (`restype=container`), and lists information about all the blobs. This list won't return the blobs themselves, but will return a URL for each blob that can be used to load the blob data.
1. Add a loadJSON() function to your script block:
> 💁 You can put this URL into your browser to see details of all the blobs in your container. Each item will have a `Url` property that you can also load in your browser to see the contents of the blob.
```javascript
var map, features;
This code then loads each blob, calling a `loadJSON` function, which will be created next. It then creates the map control, and adds code to the `ready` event. This event is called when the map is displayed on the web page.
The ready event creates an Azure Maps data source - a container that contains GeoJSON data that will be populated later. This data source is then used to create a bubble layer - that is a set of circles on the map centered over each point in the GeoJSON.
1. Add the `loadJSON` function to your script block, below the `init` function:
```javascript
var map, features;
function loadJSON(file) {
var xhr = new XMLHttpRequest();
@ -246,12 +311,16 @@ var map, features;
xhr.open("GET", file, true);
xhr.send();
}
```
```
This function is called by the fetch routine to parse through the JSON data and convert it to be read as longitude and latitude coordinates as geoJSON.
Once parsed, the data is set as part of a geoJSON `Feature`. The map will be initialized and little bubbles will appear around the path your data is plotting:
This function is called by the fetch routine to parse through the JSON data and convert it to be read as longitude and latitude coordinates as geoJSON.
Once parsed, the data is set as part of a geoJSON `Feature`. The map will be initialized and little bubbles will appear around the path your data is plotting:
![data path](images/path.png)
1. Load the HTML page in your browser. It will load the map, then load all the GPS data from storage and plot it on the map.
![A map of Saint Edward State Park near Seattle, with circles showing a path around the edge of the park](../../../images/map-path.png)
> 💁 You can find this code in the [code](./code) folder.
---
@ -265,7 +334,10 @@ It's nice to be able to display static data on a map as markers. Can you enhance
## Review & Self Study
Azure Maps is particularly useful for working with IoT devices. Research some of the uses in the [documentation](https://docs.microsoft.com/en-us/azure/azure-maps/tutorial-iot-hub-maps?WT.mc_id=academic-17441-jabenn). Deepen your knowledge of mapmaking and waypoints [with this Learn module](https://docs.microsoft.com/en-us/learn/modules/create-your-first-app-with-azure-maps/?WT.mc_id=academic-17441-jabenn).
Azure Maps is particularly useful for working with IoT devices.
* Research some of the uses in the [Azure Maps documentation on Microsoft docs](https://docs.microsoft.com/azure/azure-maps/tutorial-iot-hub-maps?WT.mc_id=academic-17441-jabenn).
* Deepen your knowledge of map making and waypoints with the [create your first route finding app with Azure Maps self-guided learning module on Microsoft Learn](https://docs.microsoft.com/learn/modules/create-your-first-app-with-azure-maps/?WT.mc_id=academic-17441-jabenn).
## Assignment

@ -1,38 +1,12 @@
<html>
<head>
<link rel="stylesheet" href="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.css" type="text/css" />
<head>
<link rel="stylesheet" href="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.css"
type="text/css" />
<script src="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.js"></script>
<script type='text/javascript'>
var map, features;
function loadJSON(file) {
//load files and build up features object
var xhr = new XMLHttpRequest();
features = [];
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
gps = JSON.parse(xhr.responseText)
features.push(
new atlas.data.Feature(new atlas.data.Point([parseFloat(gps.gps.lon), parseFloat(gps.gps.lat)]))
)
}
}
};
xhr.open("GET", file, true);
xhr.send();
}
function init() {
fetch("https://gpssensorjimb.blob.core.windows.net/gps-data/?restype=container&comp=list")
fetch("https://<storage_name>.blob.core.windows.net/gps-data/?restype=container&comp=list")
.then(response => response.text())
.then(str => new window.DOMParser().parseFromString(str, "text/xml"))
.then(xml => {
@ -41,13 +15,13 @@
loadJSON(blobUrl.innerHTML)
});
})
.then( response => {
.then(response => {
map = new atlas.Map('myMap', {
center: [-122.26473, 47.73444],
zoom: 14,
authOptions: {
authType: "subscriptionKey",
subscriptionKey: "<your-key>",
subscriptionKey: "<subscription_key>",
}
});
@ -58,13 +32,31 @@
source.add(features);
})
})
}
var map, features;
function loadJSON(file) {
var xhr = new XMLHttpRequest();
features = [];
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
gps = JSON.parse(xhr.responseText)
features.push(
new atlas.data.Feature(new atlas.data.Point([parseFloat(gps.gps.lon), parseFloat(gps.gps.lat)]))
)
}
}
};
xhr.open("GET", file, true);
xhr.send();
}
</script>
<style>
#myMap {
width:100%;
height:100%;
width: 100%;
height: 100%;
}
</style>
</head>
@ -73,6 +65,4 @@
<div id="myMap"></div>
</body>
</html>

@ -41,7 +41,7 @@ There are many reasons why you would want to know that a vehicle is inside or ou
* Preparation for unloading - getting a notification that a vehicle has arrived on-site allows a crew to be prepared to unload the vehicle, reducing vehicle waiting time. This can allow a driver to make more deliveries in a day with less waiting time.
* Tax compliance - some countries, such as New Zealand, charge road taxes for diesel vehicles based on the vehicle weight when driving on public roads only. Using geofences allows you to track the mileage driven on public roads as opposed to private roads on sites such as farms or logging areas.
* Monitoring theft - if a vehicle should only remain in a certain area such as on a farm, and it leaves the geofence, it might be being stolen.
* Monitoring theft - if a vehicle should only remain in a certain area such as on a farm, and it leaves the geofence, it might have been stolen.
* Location compliance - some parts of a work site, farm or factory may be off-limits to certain vehicles, such as keeping vehicles that carry artificial fertilizers and pesticides away from fields growing organic produce. If a geofence is entered, then a vehicle is outside of compliance and the driver can be notified.
✅ Can you think of other uses for geofences?
@ -212,7 +212,7 @@ For example, imagine GPS readings showing a vehicle was driving along a road tha
![A GPS trail showing a vehicle passing the Microsoft campus on the 520, with GPS readings along the road except for one on the campus, inside a geofence](../../../images/geofence-crossing-inaccurate-gps.png)
In the above image, there is a geofence over part of the Microsoft campus. The red line shows a truck driving along the 520, with circles to show the GPS readings. Most of these are accurate and along the 520, with one inaccurate reading inside the geofence. The is no way that reading can be correct - there are no roads for the truck to suddenly divert from the 520 onto campus, then back onto the 520. The code that checks this geofence will need to take the previous readings into consideration before acting on the results of the geofence test.
In the above image, there is a geofence over part of the Microsoft campus. The red line shows a truck driving along the 520, with circles to show the GPS readings. Most of these are accurate and along the 520, with one inaccurate reading inside the geofence. There is no way that reading can be correct - there are no roads for the truck to suddenly divert from the 520 onto campus, then back onto the 520. The code that checks this geofence will need to take the previous readings into consideration before acting on the results of the geofence test.
✅ What additional data would you need to check to see if a GPS reading could be considered correct?
@ -237,7 +237,7 @@ In the above image, there is a geofence over part of the Microsoft campus. The r
1. Use curl to make a GET request to this URL:
```sh
curl --request GET <URL>
curl --request GET '<URL>'
```
> 💁 If you get a response code of `BadRequest`, with an error of:
@ -255,7 +255,7 @@ In the above image, there is a geofence over part of the Microsoft campus. The r
"geometries": [
{
"deviceId": "gps-sensor",
"udId": "1ffb2047-6757-8c29-2c3d-da44cec55ff9",
"udId": "7c3776eb-da87-4c52-ae83-caadf980323a",
"geometryId": "1",
"distance": 999.0,
"nearestLat": 47.645875,
@ -324,6 +324,8 @@ When you create an IoT Hub, you get the `$Default` consumer group created by def
geofence gps-sensor
```
> 💁 When you ran the IoT Hub event monitor in an earlier lesson, it connected to the `$Default` consumer group. This was why you can't run the event monitor and an event trigger. If you want to run both, then you can use other consumer groups for all your function apps, and keep `$Default` for the event monitor.
### Task - create a new IoT Hub trigger
1. Add a new IoT Hub event trigger to your `gps-trigger` function app that you created in an earlier lesson. Call this function `geofence-trigger`.

@ -10,7 +10,7 @@ As advances happen in Artificial Intelligence (AI) and Machine Learning (ML), th
In these 4 lessons you'll learn how to train image-based AI models to detect fruit quality, how to use these from an IoT device, and how to run these on the edge - that is on an IoT device rather than in the cloud.
> 💁 These lessons will use some cloud resources. If you don't complete all the lessons in this project, make sure you [Clean up your project](../clean-up.md).
> 💁 These lessons will use some cloud resources. If you don't complete all the lessons in this project, make sure you [clean up your project](../clean-up.md).
## Topics

@ -60,11 +60,11 @@ Traditional programming is where you take data, apply an algorithm to the data,
![Traditional development takes input and an algorithm and gives output. Machine learning uses input and output data to train a model, and this model can take new input data to generate new output](../../../images/traditional-vs-ml.png)
Machine learning turns this around - you start with data and known outputs, and the machine learning tools work out the algorithm. You can then take that algorithm, called a *machine learning model*, and input new data and get new output.
Machine learning turns this around - you start with data and known outputs, and the machine learning algorithm learns from the data. You can then take that trained algorithm, called a *machine learning model* or *model*, and input new data and get new output.
> 🎓 The process of a machine learning tool generating a model is called *training*. The inputs and known outputs are called *training data*.
> 🎓 The process of a machine learning algorithm learning from the data is called *training*. The inputs and known outputs are called *training data*.
For example, you could give a model millions of pictures of unripe bananas as input training data, with the training output set as `unripe`, and millions of ripe banana pictures as training data with the output set as `ripe`. The ML tools will then generate a model. You then give this model a new picture of a banana and it will predict if the new picture is a ripe or an unripe banana.
For example, you could give a model millions of pictures of unripe bananas as input training data, with the training output set as `unripe`, and millions of ripe banana pictures as training data with the output set as `ripe`. The ML algorithm will then create a model based off this data. You then give this model a new picture of a banana and it will predict if the new picture is a ripe or an unripe banana.
> 🎓 The results of ML models are called *predictions*
@ -123,6 +123,8 @@ To use Custom Vision, you first need to create two cognitive services resources
This will create a Custom Vision training resource in your Resource Group. It will be called `fruit-quality-detector-training` and use the `F0` sku, which is the free tier. The `--yes` option means you agree to the terms and conditions of the cognitive services.
> 💁 Use `S0` sku if you already have a free account using any of the Cognitive Services.
1. Use the following command to create a free Custom Vision prediction resource:
```sh

@ -66,7 +66,7 @@ When you are happy with an iteration, you can publish it to make it available to
Iterations are published from the Custom Vision portal.
1. Launch the Custom Vision portal at [CustomVision.ai](https://customvision.ai) and sign in if you don't have it open already.
1. Launch the Custom Vision portal at [CustomVision.ai](https://customvision.ai) and sign in if you don't have it open already. Then open your `fruit-quality-detector` project.
1. Select the **Performance** tab from the options at the top

@ -1,5 +1,7 @@
# Run your fruit detector on the edge
<!-- This lesson is still under development -->
Add a sketchnote if possible/appropriate
This video gives an overview of running image classifiers on IoT devices, the topic that is covered in this lesson.
@ -18,9 +20,73 @@ In this lesson you will learn about
In this lesson we'll cover:
* [Thing 1](#thing-1)
* [Edge computing](#edge-computing)
* [Azure IoT Edge](#azure-iot-edge)
* [Register an IoT Edge device](#registeran-iot-edge-device)
* [Set up an IoT Edge device](#set-up-an-iot-dge-device)
* [Run your classifier on the edge](run-your-classifier-on-the-edge)
## Edge computing
## Azure IoT Edge
![The Azure IoT Edge logo](../../../images/azure-iot-edge-logo.png)
IoT Edge runs code from containers.
## Register an IoT Edge device
To use an IoT Edge device, it needs to be registered in IoT Hub.
### Task - register an IoT Edge device
1. Create an IoT Hub in the `fruit-quality-detector` resource group. Give it a unique name based around `fruit-quality-detector`.
1. Register an IoT Edge device called `fruit-quality-detector-edge` in your IoT Hub. The command to do this is similar to the one used to register a non-edge device, except you pass the `--edge-enabled` flag.
```sh
az iot hub device-identity create --edge-enabled \
--device-id fruit-quality-detector-edge \
--hub-name <hub_name>
```
Replace `<hub_name>` with the name of your IoT Hub.
1. Get the connection string for your device using the following command:
```sh
az iot hub device-identity connection-string show --device-id fruit-quality-detector-edge \
--output table \
--hub-name <hub_name>
```
Replace `<hub_name>` with the name of your IoT Hub.
Take a copy of the connection string that is shown in the output.
## Set up an IoT Edge device
### Task - set up an IoT Edge device
The IoT Edge runtime only runs Linux containers. It can be run on Linux, or on Windows using Linux Virtual Machines.
* If you are using a Raspberry Pi as your IoT device, then this runs a supported version of Linux and can host the IoT Edge runtime. Follow the [Install Azure IoT Edge for Linux guide on Microsoft docs](https://docs.microsoft.com/azure/iot-edge/how-to-install-iot-edge?WT.mc_id=academic-17441-jabenn) to install IoT Edge and set the connection string.
> 💁 Remember, Raspberry Pi OS is a variant of Debian Linux.
* If you are not using a Raspberry Pi, but have a Linux computer, you can run the IoT Edge runtime. Follow the [Install Azure IoT Edge for Linux guide on Microsoft docs](https://docs.microsoft.com/azure/iot-edge/how-to-install-iot-edge?WT.mc_id=academic-17441-jabenn) to install IoT Edge and set the connection string.
* If you are using Windows, you can install the IoT Edge runtime in a Linux Virtual Machine by following the [Install and start the IoT Edge runtime section of the Deploy your first IoT Edge module to a Windows device quickstart on Microsoft docs](https://docs.microsoft.com/azure/iot-edge/quickstart?WT.mc_id=academic-17441-jabenn#install-and-start-the-iot-edge-runtime). You can stop when you reach the *Deploy a module* section.
* If you are using macOS, you can create a virtual machine (VM) in the cloud to use for your IoT Edge device. These are computers you can create in the cloud and access over the internet. You can create a Linux VM that has IoT Edge installed. Follow the [Create a virtual machine running IoT Edge guide](vm-iotedge.md) for instructions on how to do this.
## Create a classifier that can run on the edge
## Run your classifier on the edge
### Task - deploy your classifier using IoT Edge
## Thing 1
### Task - use the edge classifier from your IoT device
---

@ -0,0 +1,66 @@
# Create a virtual machine running IoT Edge
In Azure, you can create a virtual machine - a computer in the cloud that you can configure any way you wish and run your own software on it.
> 💁 You can read more about virtual machines on teh [Virtual Machine page on Wikipedia](https://wikipedia.org/wiki/Virtual_machine).
## Task - Set up an IoT Edge virtual machine
1. Run the following command to create a VM that has Azure IoT Edge already pre-installed:
```sh
az deployment group create \
--resource-group fruit-quality-detector \
--template-uri https://raw.githubusercontent.com/Azure/iotedge-vm-deploy/1.2.0/edgeDeploy.json \
--parameters dnsLabelPrefix=<vm_name> \
--parameters adminUsername=<username> \
--parameters deviceConnectionString="<connection_string>" \
--parameters authenticationType=password \
--parameters adminPasswordOrKey="<password>"
```
Replace `<vm_name>` with a name for this virtual machine. This needs to be globally unique, so use something like `fruit-quality-detector-vm-` with your name or another value on the end.
Replace `<username>` and `<password>` with a username and password to use to log in to the VM. These need to be relatively secure, so you can't use admin/password.
Replace `<connection_string>` with the connection string of your `fruit-quality-detector-edge` IoT Edge device.
This will create a VM configured as a `DS1 v2` virtual machine. These categories indicate how powerful the machine is, and therefor how much it costs. This VM has 1 CPU and 3.5GB of RAM.
> 💰 You can see the current pricing of these VMs on the [Azure Virtual Machine pricing guide](https://azure.microsoft.com/pricing/details/virtual-machines/linux/?WT.mc_id=academic-17441-jabenn)
Once the VM has been created, the IoT Edge runtime will be installed automatically, and configured you connect to your IoT Hub as your `fruit-quality-detector-edge` device.
1. VMs cost money. At the time of writing, a DS1 VM costs about $0.06 per hour. To keep costs down, you should shut down the VM when you are not using it, and delete it when you are finished with this project.
To shut down the VM, use the following command:
```sh
az vm deallocate --resource-group fruit-quality-detector \
--name <vm_name>
```
Replace `<vm_name>` with the name of your virtual machine.
> 💁 There is an `az vm stop` command which will stop the VM, but it keeps the computer allocated to you, so you still pay as if it was still running.
To restart the VM, use the following command:
```sh
az vm start --resource-group fruit-quality-detector \
--name <vm_name>
```
Replace `<vm_name>` with the name of your virtual machine.
You can also configure your VM to automatically shut down at a certain time each day. This means if you forget to shut it down, you won't be billed for more than the time till the automatic shutdown. Use the following command to set this:
```sh
az vm auto-shutdown --resource-group fruit-quality-detector \
--name <vm_name> \
--time <shutdown_time_utc>
```
Replace `<vm_name>` with the name of your virtual machine.
Replace `<shutdown_time_utc>` with the UTC time that you want the VM to shut down using 4 digits as HHMM. For example, if you want to shutdown at midnight UTC, you would set this to `0000`. For 7:30PM on the west coast of the USA, you would use 0230 (7:30PM on the US west coast is 2:30AM UTC).

@ -111,7 +111,7 @@ You can train an object detector using Custom Vision, in a similar way to how yo
![The settings for the custom vision project with the name set to fruit-quality-detector, no description, the resource set to fruit-quality-detector-training, the project type set to classification, the classification types set to multi class and the domains set to food](../../../images/custom-vision-create-object-detector-project.png)
> 💁 The products on shelves domain is specifically targeted for detecting stock on store shelves.
The products on shelves domain is specifically targeted for detecting stock on store shelves. Read more on the different domains in the [Select a domian documentation on Microsoft Docs](https://docs.microsoft.com/azure/cognitive-services/custom-vision-service/select-domain?WT.mc_id=academic-17441-jabenn#object-detection)
✅ Take some time to explore the Custom Vision UI for your object detector.
@ -119,7 +119,7 @@ You can train an object detector using Custom Vision, in a similar way to how yo
To train your model you will need a set of images containing the objects you want to detect.
1. Gather images that contain the object to detect. You will need at least 15 images containing each object to detect from a variety of different angles and in different lighting conditions, but the more the better. You will also need a few images to test the model. If you are detecting more than one object, you will want some testing images that contain all the objects.
1. Gather images that contain the object to detect. You will need at least 15 images containing each object to detect from a variety of different angles and in different lighting conditions, but the more the better. This object detector uses the *Products on shelves* domain, so try to set up the objects as if they were on a store shelf. You will also need a few images to test the model. If you are detecting more than one object, you will want some testing images that contain all the objects.
> 💁 Images with multiple different objects count towards the 15 image minimum for all the objects in the image.
@ -188,4 +188,4 @@ If you have any similar looking items, test it out by adding images of them to y
## Assignment
[](assignment.md)
[Compare domains](assignment.md)

@ -1,9 +1,14 @@
#
# Compare domains
## Instructions
When you created your object detector, you had a choice of multiple domains. Compare how well they work for your stock detector, and describe which gives better results.
To change the domain, select the **Settings** button on the top menu, select a new domain, select the **Save changes** button, then retrain the model. Make sure you test with the new iteration of the model trained with the new domain.
## Rubric
| Criteria | Exemplary | Adequate | Needs Improvement |
| -------- | --------- | -------- | ----------------- |
| | | | |
| Train the model with a different domain | Was able to change the domain and re-train the model | Was able to change the domain and re-train the model | Was unable to change the domain or re-train the model |
| Test the model and compare the results | Was able to test the model with different domains, compare results, and describe which is better | Was able to test the model with different domains, but was unable to compare the results and describe which is better | Was unable to test the model with different domains |

@ -91,6 +91,22 @@ These samples are taken many thousands of times per second, using well-defined s
✅ Do some research: If you use a streaming music service, what sample rate and size does it use? If you use CDs, what is the sample rate and size of CD audio?
There are a number of different formats for audio data. You've probably heard of mp3 files - audio data that is compressed to make it smaller without losing any quality. Uncompressed audio is often stored as a WAV file - this is a file with 44 bytes of header information, followed by raw audio data. The header contains information such as the sample rate (for example 16000 for 16KHz) and sample size (16 for 16-bit), and the number of channels. After the header, the WAV file contains the raw audio data.
> 🎓 Channels refers to how many different audio streams make up the audio. For example, for stereo audio with left and right, there would be 2 channels. For 7.1 surround sound for a home theater system this would be 8.
### Audio data size
Audio data is relatively large. For example, capturing uncompressed 16-bit audio at 16KHz (a good enough rate for use with speech to text model), takes 32KB of data for each second of audio:
* 16-bit means 2 bytes per sample (1 byte is 8 bits).
* 16KHz is 16,000 samples per second.
* 16,000 x 2 bytes = 32,000 bytes per second.
This sounds like a small amount of data, but if you are using a microcontroller with limited memory, this can be a lot. For example, the Wio Terminal has 192KB of memory, and that needs to store program code and variables. Even if your program code was tiny, you couldn't capture more than 5 seconds of audio.
Microcontrollers can access additional storage, such as SD cards or flash memory. When building an IoT device that captures audio you will need to ensure not only you have additional storage, but your code writes the audio captured from your microphone directly to that storage, and when sending it to the cloud, you stream from storage to the web request. That way you can avoid running out of memory by trying to hold the entire block of audio data in memory at once.
## Capture audio from your IoT device
Your IoT device can be connected to a microphone to capture audio, ready for conversion to text. It can also be connected to speakers to output audio. In later lessons this will be used to give audio feedback, but it is useful to set up speakers now to test the microphone.
@ -186,26 +202,6 @@ Work through the relevant guide to convert speech to text on your IoT device:
* [Single-board computer - Raspberry Pi](pi-speech-to-text.md)
* [Single-board computer - Virtual device](virtual-device-speech-to-text.md)
### Task - send converted speech to an IoT services
To use the results of the speech to text conversion, you need to send it to the cloud. There it will be interpreted and responses sent back to the IoT device as commands.
1. Create a new IoT Hub in the `smart-timer` resource group, and register a new device called `smart-timer`.
1. Connect your IoT device to this IoT Hub using what you have learned in previous lessons, and send the speech as telemetry. Use a JSON document in this format:
```json
{
"speech" : "<converted speech>"
}
```
Where `<converted speech>` is the output from the speech to text call. You only need to send speech that has content, if the call returns an empty string it can be ignored.
1. Verify that messages are being sent by monitoring the Event Hub compatible endpoint using the `az iot hub monitor-events` command.
> 💁 You can find this code in the [code-iot-hub/virtual-iot-device](code-iot-hub/virtual-iot-device), [code-iot-hub/pi](code-iot-hub/pi), or [code-iot-hub/wio-terminal](code-iot-hub/wio-terminal) folder.
---
## 🚀 Challenge

@ -1,93 +0,0 @@
import io
import json
import pyaudio
import requests
import time
import wave
from azure.iot.device import IoTHubDeviceClient, Message
from grove.factory import Factory
button = Factory.getButton('GPIO-HIGH', 5)
audio = pyaudio.PyAudio()
microphone_card_number = 1
speaker_card_number = 1
rate = 48000
def capture_audio():
stream = audio.open(format = pyaudio.paInt16,
rate = rate,
channels = 1,
input_device_index = microphone_card_number,
input = True,
frames_per_buffer = 4096)
frames = []
while button.is_pressed():
frames.append(stream.read(4096))
stream.stop_stream()
stream.close()
wav_buffer = io.BytesIO()
with wave.open(wav_buffer, 'wb') as wavefile:
wavefile.setnchannels(1)
wavefile.setsampwidth(audio.get_sample_size(pyaudio.paInt16))
wavefile.setframerate(rate)
wavefile.writeframes(b''.join(frames))
wav_buffer.seek(0)
return wav_buffer
speech_api_key = '<key>'
location = '<location>'
language = '<language>'
connection_string = '<connection_string>'
device_client = IoTHubDeviceClient.create_from_connection_string(connection_string)
print('Connecting')
device_client.connect()
print('Connected')
def get_access_token():
headers = {
'Ocp-Apim-Subscription-Key': speech_api_key
}
token_endpoint = f'https://{location}.api.cognitive.microsoft.com/sts/v1.0/issuetoken'
response = requests.post(token_endpoint, headers=headers)
return str(response.text)
def convert_speech_to_text(buffer):
url = f'https://{location}.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1'
headers = {
'Authorization': 'Bearer ' + get_access_token(),
'Content-Type': f'audio/wav; codecs=audio/pcm; samplerate={rate}',
'Accept': 'application/json;text/xml'
}
params = {
'language': language
}
response = requests.post(url, headers=headers, params=params, data=buffer)
response_json = json.loads(response.text)
if response_json['RecognitionStatus'] == 'Success':
return response_json['DisplayText']
else:
return ''
while True:
while not button.is_pressed():
time.sleep(.1)
buffer = capture_audio()
text = convert_speech_to_text(buffer)
if len(text) > 0:
message = Message(json.dumps({ 'speech': text }))
device_client.send_message(message)

@ -1,33 +0,0 @@
import json
import time
from azure.cognitiveservices.speech import SpeechConfig, SpeechRecognizer
from azure.iot.device import IoTHubDeviceClient, Message
speech_api_key = '<key>'
location = '<location>'
language = '<language>'
connection_string = '<connection_string>'
device_client = IoTHubDeviceClient.create_from_connection_string(connection_string)
print('Connecting')
device_client.connect()
print('Connected')
recognizer_config = SpeechConfig(subscription=speech_api_key,
region=location,
speech_recognition_language=language)
recognizer = SpeechRecognizer(speech_config=recognizer_config)
def recognized(args):
if len(args.result.text) > 0:
message = Message(json.dumps({ 'speech': args.result.text }))
device_client.send_message(message)
recognizer.recognized.connect(recognized)
recognizer.start_continuous_recognition()
while True:
time.sleep(1)

@ -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,19 @@
; 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.0.3
seeed-studio/Seeed Arduino SFUD @ 2.0.1
build_flags =
-DSFUD_USING_QSPI

@ -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,49 @@
#include <Arduino.h>
#include <sfud.h>
#include <SPI.h>
#include "mic.h"
void setup()
{
Serial.begin(9600);
while (!Serial)
; // Wait for Serial to be ready
delay(1000);
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();
Serial.println("Ready.");
}
void processAudio()
{
}
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();
}
}

@ -0,0 +1,248 @@
#pragma once
#include <Arduino.h>
#include "flash_writer.h"
#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
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;
static uint16_t idx = 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,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

@ -1,5 +1,4 @@
import io
import json
import pyaudio
import requests
import time
@ -66,17 +65,20 @@ def convert_speech_to_text(buffer):
}
response = requests.post(url, headers=headers, params=params, data=buffer)
response_json = json.loads(response.text)
response_json = response.json()
if response_json['RecognitionStatus'] == 'Success':
return response_json['DisplayText']
else:
return ''
def process_text(text):
print(text)
while True:
while not button.is_pressed():
time.sleep(.1)
buffer = capture_audio()
text = convert_speech_to_text(buffer)
print(text)
process_text(text)

@ -11,8 +11,11 @@ recognizer_config = SpeechConfig(subscription=speech_api_key,
recognizer = SpeechRecognizer(speech_config=recognizer_config)
def process_text(text):
print(text)
def recognized(args):
print(args.result.text)
process_text(args.result.text)
recognizer.recognized.connect(recognized)

@ -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,22 @@
; 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.0.3
seeed-studio/Seeed Arduino SFUD @ 2.0.1
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

@ -0,0 +1,89 @@
#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 *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,69 @@
#include <Arduino.h>
#include <rpcWiFi.h>
#include <sfud.h>
#include <SPI.h>
#include "config.h"
#include "mic.h"
#include "speech_to_text.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();
Serial.println("Ready.");
}
void processAudio()
{
String text = speechToText.convertSpeechToText();
Serial.println(text);
}
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();
}
}

@ -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,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

@ -12,11 +12,10 @@ The audio can be sent to the speech service using the REST API. To use the speec
1. Remove the `play_audio` function. This is no longer needed as you don't want a smart timer to repeat back to you what you said.
1. Add the following imports to the top of the `app.py` file:
1. Add the following import to the top of the `app.py` file:
```python
import requests
import json
```
1. Add the following code above the `while True` loop to declare some settings for the speech service:
@ -74,7 +73,7 @@ The audio can be sent to the speech service using the REST API. To use the speec
```python
response = requests.post(url, headers=headers, params=params, data=buffer)
response_json = json.loads(response.text)
response_json = response.json()
if response_json['RecognitionStatus'] == 'Success':
return response_json['DisplayText']
@ -84,13 +83,20 @@ The audio can be sent to the speech service using the REST API. To use the speec
This calls the URL and decodes the JSON value that comes in the response. The `RecognitionStatus` value in the response indicates if the call was able to extract speech into text successfully, and if this is `Success` then the text is returned from the function, otherwise an empty string is returned.
1. Finally replace the call to `play_audio` in the `while True` loop with a call to the `convert_speech_to_text` function, as well as printing the text to the console:
1. Above the `while True:` loop, define a function to process the text returned from the speech to text service. This function will just print the text to the console for now.
```python
text = convert_speech_to_text(buffer)
def process_text(text):
print(text)
```
1. Finally replace the call to `play_audio` in the `while True` loop with a call to the `convert_speech_to_text` function, passing the text to the `process_text` function:
```python
text = convert_speech_to_text(buffer)
process_text(text)
```
1. Run the code. Press the button and speak into the microphone. Release the button when you are done, and the audio will be converted to text and printed to the console.
```output

@ -32,6 +32,7 @@ On Windows, Linux, and macOS, the speech services Python SDK can be used to list
1. Add the following imports to the `app,py` file:
```python
import requests
import time
from azure.cognitiveservices.speech import SpeechConfig, SpeechRecognizer
```
@ -62,11 +63,14 @@ On Windows, Linux, and macOS, the speech services Python SDK can be used to list
recognizer = SpeechRecognizer(speech_config=recognizer_config)
```
1. The speech recognizer runs on a background thread, listening for audio and converting any speech in it to text. You can get the text using a callback function - a function you define and pass to the recognizer. Every time speech is detected, the callback is called. Add the following code to define a callback that prints the text to the console, and pass this callback to the recognizer:
1. The speech recognizer runs on a background thread, listening for audio and converting any speech in it to text. You can get the text using a callback function - a function you define and pass to the recognizer. Every time speech is detected, the callback is called. Add the following code to define a callback, and pass this callback to the recognizer, as well as defining a function to process the text, writing it to the consoled:
```python
def process_text(text):
print(text)
def recognized(args):
print(args.result.text)
process_text(args.result.text)
recognizer.recognized.connect(recognized)
```

@ -1,3 +1,536 @@
# Capture audio - Wio Terminal
Coming soon!
In this part of the lesson, you will write code to capture audio on your Wio Terminal. Audio capture will be controlled by one of the buttons on the top of the Wio Terminal.
## Program the device to capture audio
You can capture audio from the microphone using C++ code. The Wio Terminal only has 192KB of RAM, not enough to capture more than a couple of seconds of audio. It also has 4MB of flash memory, so this can be used instead, saving captured audio to the flash memory.
The built-in microphone captures an analog signal, which gets converted to a digital signal that the Wio Terminal can use. When capturing audio, the data needs to be captured at the correct time - for example to capture audio at 16KHz, the audio needs to be captured exactly 16,000 times per second, with equal intervals between each sample. Rather than use your code to do this, you can use the direct memory access controller (DMAC). This is circuitry that can capture a signal from somewhere and write to memory, without interrupting your code running on the processor.
✅ Read more on DMA on the [direct memory access page on Wikipedia](https://wikipedia.org/wiki/Direct_memory_access).
![Audio from the mic goes to an ADC then to the DMAC. This writes to one buffer. When this buffer is full, it is processed and the DMAC writes to a second buffer](../../../images/dmac-adc-buffers.png)
The DMAC can capture audio from the ADC at fixed intervals, such as at 16,000 times a second for 16KHz audio. It can write this captured data to a pre-allocated memory buffer, and when this is full, make it available to your code to process. Using this memory can delay capturing audio, but you can set up multiple buffers. The DMAC writes to buffer 1, then when it's full, notifies your code to proces buffer 1, whilst the DMAC writes to buffer 2. When buffer 2 is full, it notifies your code, and goes back to writing to buffer 1. That way as long as you process each buffer in less time that it takes to fill one, you will not lose any data.
Once each buffer has been captured, it can be written to the flash memory. Flash memory needs to be written to using defined addresses, specifying where to write and how large to write, similar to updating an array of bytes in memory. Flash memory has granularity, meaning erase and writing operations rely not only on being of a fixed size, but aligning to that size. For example, if the granularity is 4096 bytes and you request an erase at address 4200, it could erase all the data from address 4096 to 8192. This means when you write the audio data to flash memory, it has to be in chunks of the correct size.
### Task - configure flash memory
1. Create a brand new Wio Terminal project using PlatformIO. Call this project `smart-timer`. Add code in the `setup` function to configure the serial port.
1. Add the following library dependencies to the `platformio.ini` file to provide access to the flash memory:
```ini
lib_deps =
seeed-studio/Seeed Arduino FS @ 2.0.3
seeed-studio/Seeed Arduino SFUD @ 2.0.1
```
1. Open the `main.cpp` file and add the following include directive for the flash memory library to the top of the file:
```cpp
#include <sfud.h>
#include <SPI.h>
```
> 🎓 SFUD stands for Serial Flash Universal Driver, and is a library designed to work with all flash memory chips
1. In the `setup` function, add the following code to set up the flash storage library:
```cpp
while (!(sfud_init() == SFUD_SUCCESS))
;
sfud_qspi_fast_read_enable(sfud_get_device(SFUD_W25Q32_DEVICE_INDEX), 2);
```
This loops until the SFUD library is initialized, then turns on fast reads. The built-in flash memory can be accessed using a Queued Serial Peripheral Interface (QSPI), a type of SPI controller that allows continuous access via a queue with minimal processor usage. This makes it faster to read and write to flash memory.
1. Create a new file in the `src` folder called `flash_writer.h`.
1. Add the following to the top of this file:
```cpp
#pragma once
#include <Arduino.h>
#include <sfud.h>
```
This includes some needed header files, including the header file for the SFUD library to interact with flash memory
1. Define a class in this new header file called `FlashWriter`:
```cpp
class FlashWriter
{
public:
private:
};
```
1. In the `private` section, add the following code:
```cpp
byte *_sfudBuffer;
size_t _sfudBufferSize;
size_t _sfudBufferPos;
size_t _sfudBufferWritePos;
const sfud_flash *_flash;
```
This defines some fields for the buffer to use to store data before writing it to the flash memory. There is a byte array, `_sfudBuffer`, to write data to, and when this is full, the data is written to flash memory. The `_sfudBufferPos` field stores the current location to write to in this buffer, and `_sfudBufferWritePos` stores the location in flash memory to write to. `_flash` is a pointer the flash memory to write to - some microcontrollers have multiple flash memory chips.
1. Add the following method to the `public` section to initialize this class:
```cpp
void init()
{
_flash = sfud_get_device_table() + 0;
_sfudBufferSize = _flash->chip.erase_gran;
_sfudBuffer = new byte[_sfudBufferSize];
_sfudBufferPos = 0;
_sfudBufferWritePos = 0;
}
```
This configures the flash memory on teh Wio Terminal to write to, and sets up the buffers based off the grain size of the flash memory. This is in an `init` method, rather than a constructor as this needs to be called after the flash memory has been set up in the `setup` function.
1. Add the following code to the `public` section:
```cpp
void writeSfudBuffer(byte b)
{
_sfudBuffer[_sfudBufferPos++] = b;
if (_sfudBufferPos == _sfudBufferSize)
{
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]);
}
}
void flushSfudBuffer()
{
if (_sfudBufferPos > 0)
{
sfud_erase_write(_flash, _sfudBufferWritePos, _sfudBufferSize, _sfudBuffer);
_sfudBufferWritePos += _sfudBufferSize;
_sfudBufferPos = 0;
}
}
```
This code defines methods to write bytes to the flash storage system. It works by writing to an in-memory buffer that is the right size for the flash memory, and when this is full, this is written to the flash memory, erasing any existing data at that location. There is also a `flushSfudBuffer` to write an incomplete buffer, as the data being captured won't be exact multiples of the grain size, so the end part of the data needs to be written.
> 💁 The end part of the data will write additional unwanted data, but this is ok as only the data needed will be read.
### Task - set up audio capture
1. Create a new file in the `src` folder called `config.h`.
1. Add the following to the top of this file:
```cpp
#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
```
This code sets up some constants for the audio capture.
| Constant | Value | Description |
| --------------------- | -----: | - |
| RATE | 16000 | The sample rate for the audio. !6,000 is 16KHz |
| SAMPLE_LENGTH_SECONDS | 4 | The length of audio to capture. This is set to 4 seconds. To record longer audio, increase this. |
| SAMPLES | 64000 | The total number of audio samples that will be captured. Set to the sample rate * the number of seconds |
| BUFFER_SIZE | 128044 | The size of the audio buffer to capture. Audio will be captured as a WAV file, which is 44 bytes of header, then 128,000 bytes of audio date (each sample is 2 bytes) |
| ADC_BUF_LEN | 1600 | The size of the buffers to use to capture audio from the DMAC |
> 💁 If you find 4 seconds is too short to request a timer, you can increase the `SAMPLE_LENGTH_SECONDS` value, and all the other values will recalculate.
1. Create a new file in the `src` folder called `mic.h`.
1. Add the following to the top of this file:
```cpp
#pragma once
#include <Arduino.h>
#include "config.h"
#include "flash_writer.h"
```
This includes some needed header files, including the `config.h` and `FlashWriter` header files.
1. Add the following to define a `Mic` class that can capture from the microphone:
```cpp
class Mic
{
public:
Mic()
{
_isRecording = false;
_isRecordingReady = false;
}
void startRecording()
{
_isRecording = true;
_isRecordingReady = false;
}
bool isRecording()
{
return _isRecording;
}
bool isRecordingReady()
{
return _isRecordingReady;
}
private:
volatile bool _isRecording;
volatile bool _isRecordingReady;
FlashWriter _writer;
};
Mic mic;
```
This class currently only has a couple of fields to track if recording has started, and if a recording is ready to be used. When the DMAC is set up, it continuously writes to memory buffers, so the `_isRecording` flag determines if these should be processed or ignored. The `_isRecordingReady` flag will be set when the required 4 seconds of audio has been captured. The `_writer` field is used to save the audio data to flash memory.
A global variable is then declared for an instance of the `Mic` class.
1. Add the following code to the `private` section of the `Mic` class:
```cpp
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];
```
This code defines a `configureDmaAdc` method that configures the DMAC, connecting it to the ADC and setting it to populate two different alternating buffers, `_adc_buf_0` and `_adc_buf_0`.
> 💁 One of the downsides of microcontroller development is the complexity of the code needed to interact with hardware, as your code runs at a very low level interacting with hardware directly. This code is more complex than what you would write for a single-board computer or desktop computer as there is no operating system to help. There are some libraries available that can simplify this, but there is still a lot of complexity.
1. Below this, add the following code:
```cpp
// 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);
}
```
This code defines the WAV header as a struct that takes up 44 bytes of memory. It writes details to it about the audio file rate, size, and number of channels. This header is then written to the flash memory
1. Below this code, add the following to declare a method to be called when the audio buffers are ready to process:
```cpp
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;
}
}
}
```
The audio buffers are arrays of 16-bit integers containing the audio from the ADC. The ADC returns 12-bit unsigned values (0-1023), so these need to be converted to 16-bit signed values, and then converted into 2 bytes to be stored as raw binary data.
These bytes are written to the flash memory buffers. The write starts at index 44 - this is the offset from the 44 bytes written as the WAV file header. Once all the bytes needed for the required audio length have been captured, the remaing data is written to the flash memory.
1. In the `public` section of the `Mic` class, add the following code:
```cpp
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;
}
}
```
This code will be called by the DMAC to tell your code to process the buffers. It checks that there is data to process, and calls the `audioCallback` method with the relevant buffer.
1. Outside the class, after the `Mic mic;` declaration, add the following code:
```cpp
void DMAC_1_Handler()
{
mic.dmaHandler();
}
```
The `DMAC_1_Handler` will be called by the DMAC when there the buffers are ready to process. This function is found by name, so just needs to exist to be called.
1. Add the following two methods to the `public` section of the `Mic` class:
```cpp
void init()
{
analogReference(AR_INTERNAL2V23);
_writer.init();
initBufferHeader();
configureDmaAdc();
}
void reset()
{
_isRecordingReady = false;
_isRecording = false;
_writer.reset();
initBufferHeader();
}
```
The `init` method contain code to initialize the `Mic` class. This method sets the correct voltage for the Mic pin, sets up the flash memory writer, writes the WAV file header, and configures the DMAC. The `reset` method resets the flash memory and re-writes the header after the audio has been captured and used.
### Task - capture audio
1. In the `main.cpp` file, and an include directive for the `mic.h` header file:
```cpp
#include "mic.h"
```
1. In the `setup` function, initialize the C button. Audio capture will start when this button is pressed, and continue for 4 seconds:
```cpp
pinMode(WIO_KEY_C, INPUT_PULLUP);
```
1. Below this, initialize the microphone, then print to the console that audio is ready to be captured:
```cpp
mic.init();
Serial.println("Ready.");
```
1. Above the `loop` function, define a function to process the captured audio. For now this does nothing, but later in this lesson it will send the speech to be converted to text:
```cpp
void processAudio()
{
}
```
1. Add the following to the `loop` function:
```cpp
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();
}
}
```
This code checks hte C button, and if this is pressed and recording hasn't started, then the `_isRecording` field of the `Mic` class is set to true. This will cause the `audioCallback` method of the `Mic` class to store audio until 4 seconds has been captured. Once 4 seconds of audio has been captured, the `_isRecording` field is set to false, and the `_isRecordingReady` field is set to true. This is then checked in the `loop` function, and when true the `processAudio` function is called, then the mic class is reset.
1. Build this code, upload it to your Wio Terminal and test it out through the serial monitor. Press the C button (the one on the left-hand side, closest to the power switch), and speak. 4 seconds of audio will be captured.
```output
--- Available filters and text transformations: colorize, debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at http://bit.ly/pio-monitor-filters
--- Miniterm on /dev/cu.usbmodem1101 9600,8,N,1 ---
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
Ready.
Starting recording...
Finished recording
```
> 💁 You can find this code in the [code-record/wio-terminal](code-record/wio-terminal) folder.
😀 Your audio recording program was a success!

@ -1,3 +1,11 @@
# Configure your microphone and speakers - Wio Terminal
Coming soon!
In this part of the lesson, you will add and speakers to your Wio Terminal. The Wio Terminal already has a microphone built-in, and this can be used to capture speech.
## Hardware
Coming soon
### Task - connect speakers
Coming soon

@ -1,3 +1,521 @@
# Speech to text - Wio Terminal
Coming soon!
In this part of the lesson, you will write code to convert speech in the captured audio to text using the speech service.
## Send the audio to the speech service
The audio can be sent to the speech service using the REST API. To use the speech service, first you need to request an access token, then use that token to access the REST API. These access tokens expire after 10 minutes, so your code should request them on a regular basis to ensure they are always up to date.
### Task - get an access token
1. Open the `smart-timer` project if it's not already open.
1. Add the following library dependencies to the `platformio.ini` file to access WiFi and handle JSON:
```ini
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
```
1. Add the following code to the `config.h` header file:
```cpp
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";
```
Replace `<SSID>` and `<PASSWORD>` with the relevant values for your WiFi.
Replace `<API_KEY>` with the API key for your speech service resource. Replace `<LOCATION>` with the location you used when you created the speech service resource.
Replace `<LANGUAGE>` with the locale name for language you will be speaking in, for example `en-GB` for English, or `zn-HK` for Cantonese. You can find a list of the supported languages and their locale names in the [Language and voice support documentation on Microsoft docs](https://docs.microsoft.com/azure/cognitive-services/speech-service/language-support?WT.mc_id=academic-17441-jabenn#speech-to-text).
The `TOKEN_URL` constant is the URL of the token issuer without the location. This will be combined with the location later to get the full URL.
1. Just like connecting to Custom Vision, you will need to use an HTTPS connection to connect to the token issuing service. To the end of `config.h`, add the following code:
```cpp
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";
```
This is the same certificate you used when connecting to Custom Vision.
1. Add an include for the WiFi header file and the config header file to the top of the `main.cpp` file:
```cpp
#include <rpcWiFi.h>
#include "config.h"
```
1. Add code to connect to WiFi in `main.cpp` above the `setup` function:
```cpp
void connectWiFi()
{
while (WiFi.status() != WL_CONNECTED)
{
Serial.println("Connecting to WiFi..");
WiFi.begin(SSID, PASSWORD);
delay(500);
}
Serial.println("Connected!");
}
```
1. Call this function from the `setup` function after the serial connection has been established:
```cpp
connectWiFi();
```
1. Create a new header file in the `src` folder called `speech_to_text.h`. In this header file, add the following code:
```cpp
#pragma once
#include <Arduino.h>
#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include "config.h"
#include "mic.h"
class SpeechToText
{
public:
private:
};
SpeechToText speechToText;
```
This includes some necessary header files for an HTTP connection, configuration and the `mic.h` header file, and defines a class called `SpeechToText`, before declaring an instance of that class that can be used later.
1. Add the following 2 fields to the `private` section of this class:
```cpp
WiFiClientSecure _token_client;
String _access_token;
```
The `_token_client` is a WiFi Client that uses HTTPS and will be used to get the access token. This token will then be stored in `_access_token`.
1. Add the following method to the `private` section:
```cpp
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;
}
```
This code builds the URL for the token issuer API using the location of the speech resource. It then creates an `HTTPClient` to make the web request, setting it up to use the WiFi client configured with the token endpoints certificate. It sets the API key as a header for the call. It then makes a POST request to get the certificate, retrying if it gets any errors. Finally the access token is returned.
1. To the `public` section, add an `init` method that sets up the token client:
```cpp
void init()
{
_token_client.setCACert(TOKEN_CERTIFICATE);
_access_token = getAccessToken();
}
```
This sets the certificate on the WiFi client, then gets the access token.
1. In `main.cpp`, add this new header file to the include directives:
```cpp
#include "speech_to_text.h"
```
1. Initialize the `SpeechToText` class at the end of the `setup` function, after the `mic.init` call but before `Ready` is written to the serial monitor:
```cpp
speechToText.init();
```
### Task - read audio from flash memory
1. In an earlier part of this lesson, the audio was recorded to the flash memory. This audio will need to be sent to the Speech Services REST API, so it needs to be read from the flash memory. It can't be loaded into an in-memory buffer as it would be too large. The `HTTPClient` class that makes REST calls can stream data using an Arduino Stream - a class that can load data in small chunks, sending the chunks one at a time as part of the request. Every time you call `read` on a stream it returns the next block of data. An Arduino stream can be created that can read from the flash memory. Create a new file called `flash_stream.h` in the `src` folder, and add the following code to it:
```cpp
#pragma once
#include <Arduino.h>
#include <HTTPClient.h>
#include <sfud.h>
#include "config.h"
class FlashStream : public Stream
{
public:
virtual size_t write(uint8_t val)
{
}
virtual int available()
{
}
virtual int read()
{
}
virtual int peek()
{
}
private:
};
```
This declares the `FlashStream` class, deriving from the Arduino `Stream` class. This is an abstract class - derived classes have to implement a few methods before the class can be instantiated, and these methods are defined in this class.
✅ Read more on Arduino Streams in the [Arduino Stream documentation](https://www.arduino.cc/reference/en/language/functions/communication/stream/)
1. Add the following fields to the `private` section:
```cpp
size_t _pos;
size_t _flash_address;
const sfud_flash *_flash;
byte _buffer[HTTP_TCP_BUFFER_SIZE];
```
This defines a temporary buffer to store data read from the flash memory, along with fields to store the current position when reading from the buffer, the current address to read from the flash memory, and the flash memory device.
1. In the `private` section, add the following method:
```cpp
void populateBuffer()
{
sfud_read(_flash, _flash_address, HTTP_TCP_BUFFER_SIZE, _buffer);
_flash_address += HTTP_TCP_BUFFER_SIZE;
_pos = 0;
}
```
This code reads from the flash memory at the current address and stores the data in a buffer. It then increments the address, so the next call reads the next block of memory. The buffer is sized based on the largest chunk that the `HTTPClient` will send to the REST API at one time.
> 💁 Erasing flash memory has to be done using the grain size, reading on the other hand does not.
1. In the `public` section of this class, add a constructor:
```cpp
FlashStream()
{
_pos = 0;
_flash_address = 0;
_flash = sfud_get_device_table() + 0;
populateBuffer();
}
```
This constructor sets up all the fields to start reading from the start of the flash memory block, and loads the first chunk of data into the buffer.
1. Implement the `write` method. This stream will only read data, so this can do nothing and return 0:
```cpp
virtual size_t write(uint8_t val)
{
return 0;
}
```
1. Implement the `peek` method. This returns the data at the current position without moving the stream along. Calling `peek` multiple times will always return the same data as long as no data is read from the stream.
```cpp
virtual int peek()
{
return _buffer[_pos];
}
```
1. Implement the `available` function. This returns how many bytes can be read from the stream, or -1 if the stream is complete. For this class, the maximum available will be no more than the HTTPClient's chunk size. When this stream is used in the HTTP client it calls this function to see how much data is available, then requests that much data to send to the REST API. We don't want each chunk to be more than the HTTP clients chunk size, so if more than that is available, the chunk size is returned. If less, then what is available is returned. Once all the data has been streamed, -1 is returned.
```cpp
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;
}
```
1. Implement the `read` method to return the next byte from the buffer, incrementing the position. If the position exceeds the size of the buffer, it populates the buffer with the next block from the flash memory and resets the position.
```cpp
virtual int read()
{
int retVal = _buffer[_pos++];
if (_pos == HTTP_TCP_BUFFER_SIZE)
{
populateBuffer();
}
return retVal;
}
```
1. In the `speech_to_text.h` header file, add an include directive for this new header file:
```cpp
#include "flash_stream.h"
```
### Task - convert the speech to text
1. The speech can be converted to text by sending the audio to the Speech Service via a REST API. This REST API has a different certificate to the token issuer, so add the following code to the `config.h` header file to define this certificate:
```cpp
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";
```
1. Add a constant to this file for the speech URL without the location. This will be combined with the location and language later to get the full URL.
```cpp
const char *SPEECH_URL = "https://%s.stt.speech.microsoft.com/speech/recognition/conversation/cognitiveservices/v1?language=%s";
```
1. In the `speech_to_text.h` header file, in the `private` section of the `SpeechToText` class, define a field for a WiFi Client using the speech certificate:
```cpp
WiFiClientSecure _speech_client;
```
1. In the `init` method, set the certificate on this WiFi Client:
```cpp
_speech_client.setCACert(SPEECH_CERTIFICATE);
```
1. Add the following code to the `public` section of the `SpeechToText` class to define a method to convert speech to text:
```cpp
String convertSpeechToText()
{
}
```
1. Add the following code to this method to create an HTTP client using the WiFi client configured with the speech certificate, and using the speech URL set with the location and language:
```cpp
char url[128];
sprintf(url, SPEECH_URL, SPEECH_LOCATION, LANGUAGE);
HTTPClient httpClient;
httpClient.begin(_speech_client, url);
```
1. Some headers need to be set on the connection:
```cpp
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");
```
This sets headers for the authorization using the access token, the audio format using the sample rate, and sets that the client expects the result as JSON.
1. After this, add the following code to make the REST API call:
```cpp
Serial.println("Sending speech...");
FlashStream stream;
int httpResponseCode = httpClient.sendRequest("POST", &stream, BUFFER_SIZE);
Serial.println("Speech sent!");
```
This creates a `FlashStream` and uses it to stream data to the REST API.
1. Below this, add the following code:
```cpp
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);
}
```
This code checks the response code.
If it is 200, the code for success, then the result is retrieved, decoded from JSON, and the `DisplayText` property is set into the `text` variable. This is the property that the text version of the speech is returned in.
If the response code is 401, then the access token has expired (these tokens only last 10 minutes). A new access token is requested, and the call is made again.
Otherwise, an error is sent to the serial monitor, and the `text` is left blank.
1. Add the following code to the end of this method to close the HTTP client and return the text:
```cpp
httpClient.end();
return text;
```
1. In `main.cpp` call this new `convertSpeechToText` method in the `processAudio` function, then log out the speech to the serial monitor:
```cpp
String text = speechToText.convertSpeechToText();
Serial.println(text);
```
1. Build this code, upload it to your Wio Terminal and test it out through the serial monitor. Press the C button (the one on the left-hand side, closest to the power switch), and speak. 4 seconds of audio will be captured, then converted to text.
```output
--- Available filters and text transformations: colorize, debug, default, direct, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at http://bit.ly/pio-monitor-filters
--- Miniterm on /dev/cu.usbmodem1101 9600,8,N,1 ---
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
Connecting to WiFi..
Connected!
Got access token.
Ready.
Starting recording...
Finished recording
Sending speech...
Speech sent!
{"RecognitionStatus":"Success","DisplayText":"Set a 2 minute and 27 second timer.","Offset":4700000,"Duration":35300000}
Set a 2 minute and 27 second timer.
```
> 💁 You can find this code in the [code-speech-to-text/wio-terminal](code-speech-to-text/wio-terminal) folder.
😀 Your speech to text program was a success!

@ -256,22 +256,38 @@ To use this model from code, you need to publish it. When publishing from LUIS,
## Use the language understanding model
Once published, the LUIS model can be called from code. In the last lesson you sent the recognized speech to an IoT Hub, and you can use serverless code to respond to this and understand what was sent.
Once published, the LUIS model can be called from code. In previous lessons, you have used an IoT Hub to handle communication with cloud services, sending telemetry and listening for commands. This is very asynchronous - once telemetry is sent your code doesn't wait for a response, and if the cloud service is down, you wouldn't know.
For a smart timer, we want a response straight away, so we can tell the user that a timer is set, or alert them that the cloud services are unavailable. To do this, our IoT device will call a web endpoint directly, instead of relying on an IoT Hub.
Rather than calling LUIS from the IoT device, you can use serverless code with a different type of trigger - an HTTP trigger. This allows your function app to listen for REST requests, and respond to them. This function will be a REST endpoint your device can call.
> 💁 Although you can call LUIS directly from your IoT device, it's better to use something like serverless code. This way when of you want to change the LUIS app that you call, for example when you train a better model or train a model in a different language, you only have to update your cloud code, not re-deploy code to potentially thousands or millions of IoT device.
### Task - create a serverless functions app
1. Create an Azure Functions app called `smart-timer-trigger`.
1. Create an Azure Functions app called `smart-timer-trigger`, and open this in VS Code
1. Add an HTTP trigger to this app called `speech-trigger` using the following command from inside the VS Code terminal:
```sh
func new --name text-to-timer --template "HTTP trigger"
```
This will crate an HTTP trigger called `text-to-timer`.
1. Add an IoT Hub event trigger to this app called `speech-trigger`.
1. Test the HTTP trigger by running the functions app. When it runs you will see the endpoint listed in the output:
1. Set the Event Hub compatible endpoint connection string for your IoT Hub in the `local.settings.json` file, and use the key for that entry in the `function.json` file.
```output
Functions:
1. Use the Azurite app as a local storage emulator.
text-to-timer: [GET,POST] http://localhost:7071/api/text-to-timer
```
1. Run your functions app and your IoT device to ensure speech is arriving at the IoT Hub.
Test this by loading the [http://localhost:7071/api/text-to-timer](http://localhost:7071/api/text-to-timer) URL in your browser.
```output
Python EventHub trigger processed an event: {"speech": "Set a 3 minute timer."}
This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.
```
### Task - use the language understanding model
@ -288,6 +304,12 @@ Once published, the LUIS model can be called from code. In the last lesson you s
pip install -r requirements.txt
```
> 💁 If you get errors, you may need to upgrade pip with the following command:
>
> ```sh
> pip install --upgrade pip
> ```
1. Add new entries to the `local.settings.json` file for your LUIS API Key, Endpoint URL, and App ID from the **MANAGE** tab of the LUIS portal:
```JSON
@ -313,7 +335,7 @@ Once published, the LUIS model can be called from code. In the last lesson you s
This imports some system libraries, as well as the libraries to interact with LUIS.
1. In the `main` method, before it loops through all the events, add the following code:
1. Delete the contents of the `main` method, and add the following code:
```python
luis_key = os.environ['LUIS_KEY']
@ -326,14 +348,19 @@ Once published, the LUIS model can be called from code. In the last lesson you s
This loads the values you added to the `local.settings.json` file for your LUIS app, creates a credentials object with your API key, then creates a LUIS client object to interact with your LUIS app.
1. Predictions are requested from LUIS by sending a prediction request - a JSON document containing the text to predict. Create this with the following code inside the `for event in events` loop:
1. This HTTP trigger will be called passing the text to understand as JSON, with the text in a property called `text`. The following code extracts the value from the body of the HTTP request, and logs it to the console. Add this code to the `main` function:
```python
event_body = json.loads(event.get_body().decode('utf-8'))
prediction_request = { 'query' : event_body['speech'] }
req_body = req.get_json()
text = req_body['text']
logging.info(f'Request - {text}')
```
This code extracts the speech that was sent to the IoT Hub and uses it to build the prediction request.
1. Predictions are requested from LUIS by sending a prediction request - a JSON document containing the text to predict. Create this with the following code:
```python
prediction_request = { 'query' : text }
```
1. This request can then be sent to LUIS, using the staging slot that your app was published to:
@ -373,7 +400,7 @@ Once published, the LUIS model can be called from code. In the last lesson you s
* *"Set a 30 second timer"* - this will have one number, `30`, and one time unit, `second` so the single number will match the single time unit.
* *"Set a 2 minute and 30 second timer"* - this will have two numbers, `2` and `30`, and two time units, `minute` and `second` so the first number will be for the first time unit (2 minutes), and the second number for the second time unit (30 seconds).
The following code gets the count of items in the number entities, and uses that to extract the first item from each array, then the second and so on:
The following code gets the count of items in the number entities, and uses that to extract the first item from each array, then the second and so on. Add this inside the `if` block.
```python
for i in range(0, len(numbers)):
@ -397,24 +424,92 @@ Once published, the LUIS model can be called from code. In the last lesson you s
total_seconds += number
```
1. Finally, outside this loop through the entities, log the total time for the timer:
1. Outside this loop through the entities, log the total time for the timer:
```python
logging.info(f'Timer required for {total_seconds} seconds')
```
1. Run the function app and speak into your IoT device. You will see the total time for the timer in the function app output:
1. The number of seconds needs to be returned from the function as an HTTP response. At the end of the `if` block, add the following:
```python
payload = {
'seconds': total_seconds
}
return func.HttpResponse(json.dumps(payload), status_code=200)
```
This code creates a payload containing the total number of seconds for the timer, converts it to a JSON string and returns it as an HTTP result with a status code of 200, which means the call was successful.
1. Finally, outside the `if` block, handle if the intent was not recognized by returning an error code:
```python
return func.HttpResponse(status_code=404)
```
404 is the status code for *not found*.
1. Run the function app and test it out using curl.
```sh
curl --request POST 'http://localhost:7071/api/text-to-timer' \
--header 'Content-Type: application/json' \
--include \
--data '{"text":"<text>"}'
```
Replace `<text>` with the text of your request, for example `set a 2 minutes 27 second timer`.
You will see the following output from the functions app:
```output
Functions:
text-to-timer: [GET,POST] http://localhost:7071/api/text-to-timer
For detailed output, run func with --verbose flag.
[2021-06-26T19:45:14.502Z] Worker process started and initialized.
[2021-06-26T19:45:19.338Z] Host lock lease acquired by instance ID '000000000000000000000000951CAE4E'.
[2021-06-26T19:45:52.059Z] Executing 'Functions.text-to-timer' (Reason='This function was programmatically called via the host APIs.', Id=f68bfb90-30e4-47a5-99da-126b66218e81)
[2021-06-26T19:45:53.577Z] Timer required for 147 seconds
[2021-06-26T19:45:53.746Z] Executed 'Functions.text-to-timer' (Succeeded, Id=f68bfb90-30e4-47a5-99da-126b66218e81, Duration=1750ms)
```
The call to curl will return the following:
```output
[2021-06-16T01:38:33.316Z] Executing 'Functions.speech-trigger' (Reason='(null)', Id=39720c37-b9f1-47a9-b213-3650b4d0b034)
[2021-06-16T01:38:33.329Z] Trigger Details: PartionId: 0, Offset: 3144-3144, EnqueueTimeUtc: 2021-06-16T01:38:32.7970000Z-2021-06-16T01:38:32.7970000Z, SequenceNumber: 8-8, Count: 1
[2021-06-16T01:38:33.605Z] Python EventHub trigger processed an event: {"speech": "Set a four minute 17 second timer."}
[2021-06-16T01:38:35.076Z] Timer required for 257 seconds
[2021-06-16T01:38:35.128Z] Executed 'Functions.speech-trigger' (Succeeded, Id=39720c37-b9f1-47a9-b213-3650b4d0b034, Duration=1894ms)
HTTP/1.1 200 OK
Date: Tue, 29 Jun 2021 01:14:11 GMT
Content-Type: text/plain; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked
{"seconds": 147}
```
The number of seconds for the timer is in the `"seconds"` value.
> 💁 You can find this code in the [code/functions](code/functions) folder.
### Task - make your function available to your IoT device
1. For your IoT device to call your REST endpoint, it will need to know the URL. When you accessed it earlier, you used `localhost`, which is a shortcut to access REST endpoints on your local machine. To allow you IoT device to get access, you need to either:
* Publish the Functions app - follow the instructions in earlier lessons to publish your functions app to the cloud. Once published, the URL will be `http://<APP_NAME>.azurewebsites.net/api/text-to-timer`, where `<APP_NAME>` will be the name of your functions app.
* Run the functions app locally, and access using the IP address - you can get the IP address of your computer on your local network, and use that to build the URL.
Find your IP address:
* On Windows 10, follow the [Find your IP address guide](https://support.microsoft.com/windows/find-your-ip-address-f21a9bbc-c582-55cd-35e0-73431160a1b9?WT.mc_id=academic-17441-jabenn)
* On macOS, follow the [How to find you IP address on a Mac guide](https://www.hellotech.com/guide/for/how-to-find-ip-address-on-mac)
* On linux, follow the section on finding your private IP address in the [How to find your IP address in Linux guide](https://opensource.com/article/18/5/how-find-ip-address-linux)
Once you have your IP address, you will able to access the function at `http://<IP_ADDRESS>:7071/api/text-to-timer`, where `<IP_ADDRESS>` will be your IP address, for example `http://192.168.1.10:7071/api/text-to-timer`.
> 💁 This will only work if your IoT device is on the same network as your computer.
1. Test the endpoint by accessing it using your browser.
---
## 🚀 Challenge
@ -429,6 +524,7 @@ There are many ways to request the same thing, such as setting a timer. Think of
* Read more about LUIS and it's capabilities on the [Language Understanding (LUIS) documentation page on Microsoft docs](https://docs.microsoft.com/azure/cognitive-services/luis/?WT.mc_id=academic-17441-jabenn)
* Read more about language understanding on the [Natural-language understanding page on Wikipedia](https://wikipedia.org/wiki/Natural-language_understanding)
* Read more on HTTP triggers in the [Azure Functions HTTP trigger documentation on Microsoft docs](https://docs.microsoft.com/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=python&WT.mc_id=academic-17441-jabenn)
## Assignment

@ -4,7 +4,7 @@
So far in this lesson you have trained a model to understand setting a timer. Another useful feature is cancelling a timer - maybe your bread is ready and can be taken out of the oven before the timer is elapsed.
Add a new intent to your LUIS app to cancel the timer. It won't need any entities, but will need some example sentences. Handle this in your serverless code if it is the top intent, logging that the intent was recognized.
Add a new intent to your LUIS app to cancel the timer. It won't need any entities, but will need some example sentences. Handle this in your serverless code if it is the top intent, logging that the intent was recognized and returning an appropriate response.
## Rubric

@ -2,8 +2,7 @@
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "python",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"IOT_HUB_CONNECTION_STRING": "<connection string>",
"AzureWebJobsStorage": "",
"LUIS_KEY": "<primary key>",
"LUIS_ENDPOINT_URL": "<endpoint url>",
"LUIS_APP_ID": "<app id>"

@ -1,43 +0,0 @@
from typing import List
import logging
import azure.functions as func
import json
import os
from azure.cognitiveservices.language.luis.runtime import LUISRuntimeClient
from msrest.authentication import CognitiveServicesCredentials
def main(events: List[func.EventHubEvent]):
luis_key = os.environ['LUIS_KEY']
endpoint_url = os.environ['LUIS_ENDPOINT_URL']
app_id = os.environ['LUIS_APP_ID']
credentials = CognitiveServicesCredentials(luis_key)
client = LUISRuntimeClient(endpoint=endpoint_url, credentials=credentials)
for event in events:
logging.info('Python EventHub trigger processed an event: %s',
event.get_body().decode('utf-8'))
event_body = json.loads(event.get_body().decode('utf-8'))
prediction_request = { 'query' : event_body['speech'] }
prediction_response = client.prediction.get_slot_prediction(app_id, 'Staging', prediction_request)
if prediction_response.prediction.top_intent == 'set timer':
numbers = prediction_response.prediction.entities['number']
time_units = prediction_response.prediction.entities['time unit']
total_seconds = 0
for i in range(0, len(numbers)):
number = numbers[i]
time_unit = time_units[i][0]
if time_unit == 'minute':
total_seconds += number * 60
else:
total_seconds += number
logging.info(f'Timer required for {total_seconds} seconds')

@ -1,15 +0,0 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"type": "eventHubTrigger",
"name": "events",
"direction": "in",
"eventHubName": "samples-workitems",
"connection": "IOT_HUB_CONNECTION_STRING",
"cardinality": "many",
"consumerGroup": "$Default",
"dataType": "binary"
}
]
}

@ -0,0 +1,46 @@
import logging
import azure.functions as func
import json
import os
from azure.cognitiveservices.language.luis.runtime import LUISRuntimeClient
from msrest.authentication import CognitiveServicesCredentials
def main(req: func.HttpRequest) -> func.HttpResponse:
luis_key = os.environ['LUIS_KEY']
endpoint_url = os.environ['LUIS_ENDPOINT_URL']
app_id = os.environ['LUIS_APP_ID']
credentials = CognitiveServicesCredentials(luis_key)
client = LUISRuntimeClient(endpoint=endpoint_url, credentials=credentials)
req_body = req.get_json()
text = req_body['text']
logging.info(f'Request - {text}')
prediction_request = { 'query' : text }
prediction_response = client.prediction.get_slot_prediction(app_id, 'Staging', prediction_request)
if prediction_response.prediction.top_intent == 'set timer':
numbers = prediction_response.prediction.entities['number']
time_units = prediction_response.prediction.entities['time unit']
total_seconds = 0
for i in range(0, len(numbers)):
number = numbers[i]
time_unit = time_units[i][0]
if time_unit == 'minute':
total_seconds += number * 60
else:
total_seconds += number
logging.info(f'Timer required for {total_seconds} seconds')
payload = {
'seconds': total_seconds
}
return func.HttpResponse(json.dumps(payload), status_code=200)
return func.HttpResponse(status_code=404)

@ -0,0 +1,20 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
]
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}

@ -72,34 +72,11 @@ These large ML models are being trained to combine all three steps into end-to-e
## Set the timer
The timer can be set by sending a command from the serverless code, instructing the IoT device to set the timer. This command will contain the time in seconds till the timer needs to go off.
To set the timer, your IoT device needs to call the REST endpoint you created using serverless code, then use the resulting number of seconds to set a timer.
### Task - set the timer using a command
### Task - call the serverless function to get the timer time
1. In your serverless code, add code to send a direct method request to your IoT device
> ⚠️ You can refer to [the instructions for sending direct method requests in lesson 5 of the farm project if needed](../../../2-farm/lessons/5-migrate-application-to-the-cloud/README.md#send-direct-method-requests-from-serverless-code).
You will need to set up the connection string for the IoT Hub with the service policy (*NOT* the device) in your `local.settings.json` file and add the `azure-iot-hub` pip package to your `requirements.txt` file. The device ID can be extracted from the event.
1. The direct method you send needs to be called `set-timer`, and will need to send the length of the timer as a JSON property called `seconds`. Use the following code to build the `CloudToDeviceMethod` using the `total_seconds` calculated from the data extracted by LUIS:
```python
payload = {
'seconds': total_seconds
}
direct_method = CloudToDeviceMethod(method_name='set-timer', payload=json.dumps(payload))
```
> 💁 You can find this code in the [code-command/functions](code-command/functions) folder.
### Task - respond to the command on the IoT device
1. On your IoT device, respond to the command.
> ⚠️ You can refer to [the instructions for handling direct method requests from IoT devices in lesson 4 of the farm project if needed](../../../2-farm/lessons/4-migrate-your-plant-to-the-cloud#task---connect-your-iot-device-to-the-cloud).
1. Work through the relevant guide to set a timer for the required time:
Follow the relevant guide to call the REST endpoint from your IoT device and set a timer for the required time:
* [Arduino - Wio Terminal](wio-terminal-set-timer.md)
* [Single-board computer - Raspberry Pi/Virtual IoT device](single-board-computer-set-timer.md)

@ -1,15 +0,0 @@
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[2.*, 3.0.0)"
}
}

@ -1,12 +0,0 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "python",
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"IOT_HUB_CONNECTION_STRING": "<connection string>",
"LUIS_KEY": "<primary key>",
"LUIS_ENDPOINT_URL": "<endpoint url>",
"LUIS_APP_ID": "<app id>",
"REGISTRY_MANAGER_CONNECTION_STRING": "<connection string>"
}
}

@ -1,4 +0,0 @@
# Do not include azure-functions-worker as it may conflict with the Azure Functions platform
azure-functions
azure-cognitiveservices-language-luis

@ -1,60 +0,0 @@
from typing import List
import logging
import azure.functions as func
import json
import os
from azure.cognitiveservices.language.luis.runtime import LUISRuntimeClient
from msrest.authentication import CognitiveServicesCredentials
from azure.iot.hub import IoTHubRegistryManager
from azure.iot.hub.models import CloudToDeviceMethod
def main(events: List[func.EventHubEvent]):
luis_key = os.environ['LUIS_KEY']
endpoint_url = os.environ['LUIS_ENDPOINT_URL']
app_id = os.environ['LUIS_APP_ID']
registry_manager_connection_string = os.environ['REGISTRY_MANAGER_CONNECTION_STRING']
credentials = CognitiveServicesCredentials(luis_key)
client = LUISRuntimeClient(endpoint=endpoint_url, credentials=credentials)
for event in events:
logging.info('Python EventHub trigger processed an event: %s',
event.get_body().decode('utf-8'))
device_id = event.iothub_metadata['connection-device-id']
event_body = json.loads(event.get_body().decode('utf-8'))
prediction_request = { 'query' : event_body['speech'] }
prediction_response = client.prediction.get_slot_prediction(app_id, 'Staging', prediction_request)
if prediction_response.prediction.top_intent == 'set timer':
numbers = prediction_response.prediction.entities['number']
time_units = prediction_response.prediction.entities['time unit']
total_seconds = 0
for i in range(0, len(numbers)):
number = numbers[i]
time_unit = time_units[i][0]
if time_unit == 'minute':
total_seconds += number * 60
else:
total_seconds += number
logging.info(f'Timer required for {total_seconds} seconds')
payload = {
'seconds': total_seconds
}
direct_method = CloudToDeviceMethod(method_name='set-timer', payload=json.dumps(payload))
registry_manager_connection_string = os.environ['REGISTRY_MANAGER_CONNECTION_STRING']
registry_manager = IoTHubRegistryManager(registry_manager_connection_string)
registry_manager.invoke_device_method(device_id, direct_method)

@ -1,15 +0,0 @@
{
"scriptFile": "__init__.py",
"bindings": [
{
"type": "eventHubTrigger",
"name": "events",
"direction": "in",
"eventHubName": "samples-workitems",
"connection": "IOT_HUB_CONNECTION_STRING",
"cardinality": "many",
"consumerGroup": "$Default",
"dataType": "binary"
}
]
}

@ -6,8 +6,6 @@ import time
import wave
import threading
from azure.iot.device import IoTHubDeviceClient, Message, MethodResponse
from grove.factory import Factory
button = Factory.getButton('GPIO-HIGH', 5)
@ -45,13 +43,6 @@ def capture_audio():
speech_api_key = '<key>'
location = '<location>'
language = '<language>'
connection_string = '<connection_string>'
device_client = IoTHubDeviceClient.create_from_connection_string(connection_string)
print('Connecting')
device_client.connect()
print('Connected')
def get_access_token():
headers = {
@ -83,6 +74,28 @@ def convert_speech_to_text(buffer):
else:
return ''
def get_timer_time(text):
url = '<URL>'
body = {
'text': text
}
response = requests.post(url, json=body)
if response.status_code != 200:
return 0
payload = response.json()
return payload['seconds']
def process_text(text):
print(text)
seconds = get_timer_time(text)
if seconds > 0:
create_timer(seconds)
def get_voice():
url = f'https://{location}.tts.speech.microsoft.com/cognitiveservices/voices/list'
@ -167,18 +180,10 @@ def handle_method_request(request):
if seconds > 0:
create_timer(payload['seconds'])
method_response = MethodResponse.create_from_method_request(request, 200)
device_client.send_method_response(method_response)
device_client.on_method_request_received = handle_method_request
while True:
while not button.is_pressed():
time.sleep(.1)
buffer = capture_audio()
text = convert_speech_to_text(buffer)
if len(text) > 0:
print(text)
message = Message(json.dumps({ 'speech': text }))
device_client.send_message(message)
process_text(text)

@ -1,19 +1,11 @@
import json
import requests
import threading
import time
from azure.cognitiveservices.speech import SpeechConfig, SpeechRecognizer, SpeechSynthesizer
from azure.iot.device import IoTHubDeviceClient, Message, MethodResponse
speech_api_key = '<key>'
location = '<location>'
language = '<language>'
connection_string = '<connection_string>'
device_client = IoTHubDeviceClient.create_from_connection_string(connection_string)
print('Connecting')
device_client.connect()
print('Connected')
recognizer_config = SpeechConfig(subscription=speech_api_key,
region=location,
@ -21,24 +13,6 @@ recognizer_config = SpeechConfig(subscription=speech_api_key,
recognizer = SpeechRecognizer(speech_config=recognizer_config)
def recognized(args):
if len(args.result.text) > 0:
message = Message(json.dumps({ 'speech': args.result.text }))
device_client.send_message(message)
recognizer.recognized.connect(recognized)
recognizer.start_continuous_recognition()
speech_config = SpeechConfig(subscription=speech_api_key,
region=location)
speech_config.speech_synthesis_language = language
speech_synthesizer = SpeechSynthesizer(speech_config=speech_config)
voices = speech_synthesizer.get_voices_async().get().voices
first_voice = next(x for x in voices if x.locale.lower() == language.lower())
speech_config.speech_synthesis_voice_name = first_voice.short_name
def say(text):
ssml = f'<speak version=\'1.0\' xml:lang=\'{language}\'>'
ssml += f'<voice xml:lang=\'{language}\' name=\'{first_voice.short_name}\'>'
@ -70,17 +44,43 @@ def create_timer(total_seconds):
announcement += 'timer started.'
say(announcement)
def handle_method_request(request):
if request.name == 'set-timer':
payload = json.loads(request.payload)
seconds = payload['seconds']
def get_timer_time(text):
url = '<URL>'
body = {
'text': text
}
response = requests.post(url, json=body)
if response.status_code != 200:
return 0
payload = response.json()
return payload['seconds']
def process_text(text):
print(text)
seconds = get_timer_time(text)
if seconds > 0:
create_timer(payload['seconds'])
create_timer(seconds)
def recognized(args):
process_text(args.result.text)
recognizer.recognized.connect(recognized)
method_response = MethodResponse.create_from_method_request(request, 200)
device_client.send_method_response(method_response)
recognizer.start_continuous_recognition()
device_client.on_method_request_received = handle_method_request
speech_config = SpeechConfig(subscription=speech_api_key,
region=location)
speech_config.speech_synthesis_language = language
speech_synthesizer = SpeechSynthesizer(speech_config=speech_config)
voices = speech_synthesizer.get_voices_async().get().voices
first_voice = next(x for x in voices if x.locale.lower() == language.lower())
speech_config.speech_synthesis_voice_name = first_voice.short_name
while True:
time.sleep(1)

@ -1,12 +1,9 @@
import io
import json
import pyaudio
import requests
import threading
import time
import wave
import threading
from azure.iot.device import IoTHubDeviceClient, Message, MethodResponse
from grove.factory import Factory
button = Factory.getButton('GPIO-HIGH', 5)
@ -45,13 +42,6 @@ def capture_audio():
speech_api_key = '<key>'
location = '<location>'
language = '<language>'
connection_string = '<connection_string>'
device_client = IoTHubDeviceClient.create_from_connection_string(connection_string)
print('Connecting')
device_client.connect()
print('Connected')
def get_access_token():
headers = {
@ -76,13 +66,28 @@ def convert_speech_to_text(buffer):
}
response = requests.post(url, headers=headers, params=params, data=buffer)
response_json = json.loads(response.text)
response_json = response.json()
if response_json['RecognitionStatus'] == 'Success':
return response_json['DisplayText']
else:
return ''
def get_timer_time(text):
url = '<URL>'
body = {
'text': text
}
response = requests.post(url, json=body)
if response.status_code != 200:
return 0
payload = response.json()
return payload['seconds']
def say(text):
print(text)
@ -98,6 +103,7 @@ def announce_timer(minutes, seconds):
def create_timer(total_seconds):
minutes, seconds = divmod(total_seconds, 60)
threading.Timer(total_seconds, announce_timer, args=[minutes, seconds]).start()
announcement = ''
if minutes > 0:
announcement += f'{minutes} minute '
@ -106,17 +112,12 @@ def create_timer(total_seconds):
announcement += 'timer started.'
say(announcement)
def handle_method_request(request):
if request.name == 'set-timer':
payload = json.loads(request.payload)
seconds = payload['seconds']
if seconds > 0:
create_timer(payload['seconds'])
method_response = MethodResponse.create_from_method_request(request, 200)
device_client.send_method_response(method_response)
def process_text(text):
print(text)
device_client.on_method_request_received = handle_method_request
seconds = get_timer_time(text)
if seconds > 0:
create_timer(seconds)
while True:
while not button.is_pressed():
@ -124,7 +125,4 @@ while True:
buffer = capture_audio()
text = convert_speech_to_text(buffer)
if len(text) > 0:
print(text)
message = Message(json.dumps({ 'speech': text }))
device_client.send_message(message)
process_text(text)

@ -1,19 +1,11 @@
import json
import requests
import threading
import time
from azure.cognitiveservices.speech import SpeechConfig, SpeechRecognizer
from azure.iot.device import IoTHubDeviceClient, Message, MethodResponse
speech_api_key = '<key>'
location = '<location>'
language = '<language>'
connection_string = '<connection_string>'
device_client = IoTHubDeviceClient.create_from_connection_string(connection_string)
print('Connecting')
device_client.connect()
print('Connected')
recognizer_config = SpeechConfig(subscription=speech_api_key,
region=location,
@ -21,14 +13,20 @@ recognizer_config = SpeechConfig(subscription=speech_api_key,
recognizer = SpeechRecognizer(speech_config=recognizer_config)
def recognized(args):
if len(args.result.text) > 0:
message = Message(json.dumps({ 'speech': args.result.text }))
device_client.send_message(message)
def get_timer_time(text):
url = '<URL>'
recognizer.recognized.connect(recognized)
body = {
'text': text
}
recognizer.start_continuous_recognition()
response = requests.post(url, json=body)
if response.status_code != 200:
return 0
payload = response.json()
return payload['seconds']
def say(text):
print(text)
@ -45,6 +43,7 @@ def announce_timer(minutes, seconds):
def create_timer(total_seconds):
minutes, seconds = divmod(total_seconds, 60)
threading.Timer(total_seconds, announce_timer, args=[minutes, seconds]).start()
announcement = ''
if minutes > 0:
announcement += f'{minutes} minute '
@ -53,17 +52,19 @@ def create_timer(total_seconds):
announcement += 'timer started.'
say(announcement)
def handle_method_request(request):
if request.name == 'set-timer':
payload = json.loads(request.payload)
seconds = payload['seconds']
def process_text(text):
print(text)
seconds = get_timer_time(text)
if seconds > 0:
create_timer(payload['seconds'])
create_timer(seconds)
method_response = MethodResponse.create_from_method_request(request, 200)
device_client.send_method_response(method_response)
def recognized(args):
process_text(args.result.text)
device_client.on_method_request_received = handle_method_request
recognizer.recognized.connect(recognized)
recognizer.start_continuous_recognition()
while True:
time.sleep(1)

@ -4,21 +4,59 @@ In this part of the lesson, you will set a timer on your virtual IoT device or R
## Set a timer
The command sent from the serverless function contains the time for the timer in seconds as the payload. This time can be used to set a timer.
The text that comes back from the speech to text call needs to be sent to your serverless code to be processed by LUIS, getting back the number of seconds for the timer. This number of seconds can be used to set a timer.
Timers can be set using the Python `threading.Timer` class. This class takes a delay time and a function, and after the delay time, the function is executed.
### Task - set a timer
### Task - send the text to the serverless function
1. Open the `smart-timer` project in VS Code, and ensure the virtual environment is loaded in the terminal if you are using a virtual IoT device.
1. Above the `process_text` function, declare a function called `get_timer_time` to call the REST endpoint you created:
```python
def get_timer_time(text):
```
1. Add the following code to this function to define the URL to call:
```python
url = '<URL>'
```
Replace `<URL>` with the URL of your rest endpoint that you built in the last lesson, either on your computer or in the cloud.
1. Add the following code to set the text as a property passed as JSON to the call:
```python
body = {
'text': text
}
response = requests.post(url, json=body)
```
1. Below this, retrieve the `seconds` from the response payload, returning 0 if the call failed:
```python
if response.status_code != 200:
return 0
payload = response.json()
return payload['seconds']
```
Successful HTTP calls return a status code in the 200 range, and your serverless code returns 200 if the text was processed and recognized as the set timer intent.
### Task - set a timer on a background thread
1. Add the following import statement at the top of the file to import the threading Python library:
```python
import threading
```
1. Above the `handle_method_request` function that handles the method request, add a function to speak a response. Fow now this will just write to the console, but later in this lesson this will speak the text.
1. Above the `process_text` function, add a function to speak a response. Fow now this will just write to the console, but later in this lesson this will speak the text.
```python
def say(text):
@ -43,9 +81,9 @@ Timers can be set using the Python `threading.Timer` class. This class takes a d
1. Below this, add the following `create_timer` function to create a timer:
```python
def create_timer(seconds):
minutes, seconds = divmod(seconds, 60)
threading.Timer(seconds, announce_timer, args=[minutes, seconds]).start()
def create_timer(total_seconds):
minutes, seconds = divmod(total_seconds, 60)
threading.Timer(total_seconds, announce_timer, args=[minutes, seconds]).start()
```
This function takes the total number of seconds for the timer that will be sent in the command, and converts this to minutes and seconds. It then creates and starts a timer object using the total number of seconds, passing in the `announce_timer` function and a list containing the minutes and seconds. When the timer elapses, it will call the `announce_timer` function, and pass the contents of this list as the parameters - so the first item in the list gets passes as the `minutes` parameter, and the second item as the `seconds` parameter.
@ -64,32 +102,23 @@ Timers can be set using the Python `threading.Timer` class. This class takes a d
Again, this only includes the time unit that has a value. This sentence is then sent to the `say` function.
1. At the start of the `handle_method_request` function, add the following code to check that the `set-timer` direct method was requested:
```python
if request.name == 'set-timer':
```
1. Inside this `if` statement, extract the timer time in seconds from the payload and use this to create a timer:
1. Add the following to the end of the `process_text` function to get the time for the timer from the text, then create the timer:
```python
payload = json.loads(request.payload)
seconds = payload['seconds']
seconds = get_timer_time(text)
if seconds > 0:
create_timer(payload['seconds'])
create_timer(seconds)
```
The timer is only created if the number of seconds is greater than 0
The timer is only created if the number of seconds is greater than 0.
1. Run the app, and ensure the function app is also running. Set some timers, and the output will show the timer being set, and then will show when it elapses:
```output
pi@raspberrypi:~/smart-timer $ python3 app.py
Connecting
Connected
Set a one minute 4 second timer.
1 minute 4 second timer started.
Times up on your 1 minute 4 second timer.
Set a two minute 27 second timer.
2 minute 27 second timer started.
Times up on your 2 minute 27 second timer.
```
> 💁 You can find this code in the [code-timer/pi](code-timer/pi) or [code-timer/virtual-iot-device](code-timer/virtual-iot-device) folder.

@ -8,10 +8,6 @@
[![GitHub forks](https://img.shields.io/github/forks/microsoft/IoT-For-Beginners.svg?style=social&label=Fork&maxAge=2592000)](https://GitHub.com/microsoft/IoT-For-Beginners/network/)
[![GitHub stars](https://img.shields.io/github/stars/microsoft/IoT-For-Beginners.svg?style=social&label=Star&maxAge=2592000)](https://GitHub.com/microsoft/IoT-For-Beginners/stargazers/)
![Under development animated GIF](https://media.giphy.com/media/3o7qE1YN7aBOFPRw8E/giphy.gif)
**This repo is under heavy development. Check back soon for more updates.**
# IoT for Beginners - A Curriculum
Azure Cloud Advocates at Microsoft are pleased to offer a 12-week, 24-lesson curriculum all about IoT basics. Each lesson includes pre- and post-lesson quizzes, written instructions to complete the lesson, a solution, an assignment and more. Our project-based pedagogy allows you to learn while building, a proven way for new skills to 'stick'.
@ -22,7 +18,7 @@ The projects cover the journey of food from farm to table. This includes farming
**Hearty thanks to our authors [Jen Fox](https://github.com/jenfoxbot), [Jen Looper](https://github.com/jlooper), [Jim Bennett](https://github.com/jimbobbennett), and our sketchnote artist [Nitya Narasimhan](https://github.com/nitya).**
**Thanks as well to our team of [Microsoft Learn Student Ambassadors](https://studentambassadors.microsoft.com?WT.mc_id=academic-17441-jabenn) who have been reviewing and translating this curriculum - [Aditya Garg](https://github.com/AdityaGarg00), [Aryan Jain](https://www.linkedin.com/in/aryan-jain-47a4a1145/), [Bhavesh Suneja](https://github.com/EliteWarrior315), [Lateefah Bello](https://www.linkedin.com/in/lateefah-bello/), [Manvi Jha](https://github.com/Severus-Matthew), [Mireille Tan](https://www.linkedin.com/in/mireille-tan-a4834819a/), [Mohammad Iftekher (Iftu) Ebne Jalal](https://github.com/Iftu119), [Priyanshu Srivastav](https://www.linkedin.com/in/priyanshu-srivastav-b067241ba), [Thanmai Gowducheruvu](https://github.com/innovation-platform), and [Zina Kamel](https://www.linkedin.com/in/zina-kamel/).**
**Thanks as well to our team of [Microsoft Learn Student Ambassadors](https://studentambassadors.microsoft.com?WT.mc_id=academic-17441-jabenn) who have been reviewing and translating this curriculum - [Aditya Garg](https://github.com/AdityaGarg00), [Arpita Das](https://github.com/Arpiiitaaa), [Aryan Jain](https://www.linkedin.com/in/aryan-jain-47a4a1145/), [Bhavesh Suneja](https://github.com/EliteWarrior315), [Lateefah Bello](https://www.linkedin.com/in/lateefah-bello/), [Manvi Jha](https://github.com/Severus-Matthew), [Mireille Tan](https://www.linkedin.com/in/mireille-tan-a4834819a/), [Mohammad Iftekher (Iftu) Ebne Jalal](https://github.com/Iftu119), [Priyanshu Srivastav](https://www.linkedin.com/in/priyanshu-srivastav-b067241ba), [Thanmai Gowducheruvu](https://github.com/innovation-platform), and [Zina Kamel](https://www.linkedin.com/in/zina-kamel/).**
> **Teachers**, we have [included some suggestions](for-teachers.md) on how to use this curriculum. If you would like to create your own lessons, we have also included a [lesson template](lesson-template/README.md).

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Before

Width:  |  Height:  |  Size: 571 KiB

After

Width:  |  Height:  |  Size: 571 KiB

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 467 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 MiB

Loading…
Cancel
Save