Companies are starting to return back to their offices if COVID is down.
They need to keep their environments clean, and their employees safe. They also need to comply with sometimes complex local regulations and prove evidences they have met the rules. Sometimes they even need to pay a great amount of fine if they forget to keep places clean (as they risk the health of their own employees).
The goal will be to keep office rooms clean regularly, never miss a room to be cleaned in time, collect data for compliance reports.
Another goal is easy installation. If a company wants to go back to Office, the physical parts should be able to be installed very quickly and easily.
A 'Room' can be any place within the Office, like toilets, cafeteria, corridors, meeting rooms, office spaces, reception, etc. even places outside the building.
Current solutionsWhat we can see in most places (especially in public toilets), is a sheet of paper where cleaning staff writes manually if the room was cleaned. These papers then - again - manually collected end of day and entered for reports. The issue with current solutions is that it is easy to forget to clean the Rooms, and also it is overwhelming task to collect all the reports and check if everything was compliant.
Put one AWS IoT EduKit into every Room. Stick them on the wall. Once they are connected to the company WiFi and registered themselves, they are ready to measure the cleaning cycle (e.g. 1 hours). Every time a cleaning staff cleans the Room, s/he pushes the button on the IoT Thing stating that the Room is ready. Then the IoT Thing sends this event to AWS, and starts counting down to the next cleaning, using display, and LED indicators. AWS lambda connects the AWS IoT Core with AWS Honeycode and Status data is stored in AWS Honeycode table.
- Green - Room is OK
- Yellow - 15 or less minutess left until next cleaning
- Red - Room should be cleaned now
Never miss a Room to be cleaned in time
As mentioned above the LEDs show locally if it is time for cleaning. But sometimes cleaning staff is not close. Thus cleaning staff has a mobile App where s/he can see the assigned Rooms and remaining times per room (also color coded), so s/he knows where to clean next.
There is also a possibility to hit the Done button on the mobile App, but only if the target Room does not have an IoT Thing installed yet. Once it is installed, the button is disabled on the phone, requiring to be present in the Room when cleaning it.
Compliance report
There is an app for managers and regulators as well. This shows cleaning logs from AWS Honeycode cleaning log table. Color indicators here also show if we were compliant or not.
Generalization
This solution can be used for any other public spaces, like Hotels, and Conference Halls as well. Not just for Offices.
Technical details and steps to implement the solutionFor the steps below we are building above architecture, building from bottom to top layers.
Step 0 is about preparing all tools and configuration needed for the project.
Steps 1 - 4 describe the path:
- Database <=> GUI <=> Mobile App
Steps 5 - 9 describe the path:
- Database <= Lambda <= IoT Rule <= IoT Shadow <= IoT Thing
Go trough the Getting Started, Cloud Connected Blinky and Smart Thermostat chapters of the AWS IoT EduKit tutorial
This will prepare:
- development environment with Visual Studio Code and PlatformIO extension
- Silicon Labs USB to UART bridge to connect IoT Thing to the host machine
- AWS account and AWS CLI configured to it
- retrieve the Device Certificate and register AWS IoT Thing
- configuration of ESP32 Firmware which sets values into sdkconfig file, like: AWS IoT endpoint hostname, Wifi SSID and password
- AWS IoT Core Shadow connection between the IoT Thing and AWS IoT Core
- an example AWS IoT Core Rule which reacts to Shadow updates
AWS Honeycode is a no-code Application builder tool which is based on tables. As such we need to go trough with screenshots how the application was built. There will be a screenshot for all important configuration.
First let's import the Field Service Agent Template which is a good start and it will be modified in the upcoming steps.
Rename the application to IoT Rooms.
Then edit the A_WorkOrders table like in the screenshot below:
- Add your Rooms to the Title column
- You can add Notes
- Set All states to Open in the Status column
- Add cleaning staff members to the Agent column (you may need to invite your team members to Honeycode before)
- Add any date in the Due column (this represents when it is due to clean the room)
- Add all of your AWS IoT Things clientId to the IoT column, to the Room where you have installed them (marked with red line in the screenshot). The clientId can be seen in the console log of the Smart Thermostat tutorial, like this:
␛[0;32mI (3425) MAIN: Device client Id: >> 0123456789abcdef01 <<␛[0m
M_Status table should look like this:
Create a new table called L_Log like the one below:
- Add Title, Notes, Agent, Due and Created columns
- Do not add any data for now, it will be automatically added by a Honeycode event we create in the next step.
In AWS Honeycode various events can be defined which are used for automating some actions.
First let's create an Event called ExtendRoomIfCleaned. This will extend the cleaning Due date in the A_WorkOrders table with 1 hour from now if then cleaning staff set the Status to Cleaned.
See below screenshots how it was set:
Then let's create the UpdateLogTable event, which will create a row into the L_Log table if Due date was changed (e.g. by the ExtendRoomIfCleaned event). This will be used to produce data for reporting.
The columns of the new row are filled as below:
- Take data from: =[Title] and write to: =[Title]
- Take data from: =[Notes] and write to: =[Notes]
- Take data from: =[Agent] and write to: =[Agent]
- Take data from: =[$PREVIOUS] and write to: =[Due]-- we save the previous value of Due, the time when it was Due to clean
- Take data from: =NOW() and write to: =[Cleaned]-- we save the current time, so we can compare it with Due in reports
Test
Navigate to the A_WorkOrders table, and change the status of one of the Rooms to Cleaned.
- Due should be changed to 1 hour from now
- Status should change back to Open
- A new log row should be created in the L_Log table
In the next step we modify the Field Service Agent App for our goals.
There is a control which is called WorkOrdersList, source of this should be: =Filter(A_WorkOrders, "ORDER BY A_WorkOrders[Due]"). This will show and order the work orders from A_WorkOrders table by Due time.
If 'Agent' is added to Personalization, that means only Rooms for the logged in user will be displayed on the app.
Colorized backgrounds can be set for Segment1. Follow the conditional formatting rules below:
- Red : =60*HOUR(MAX(0, [Due]-$[LocalNow]))+MINUTE(MAX(0, [Due]-$[LocalNow]))=0
- Yellow : =60*HOUR(MAX(0, [Due]-$[LocalNow]))+MINUTE(MAX(0, [Due]-$[LocalNow]))<=15
- Green : =60*HOUR(MAX(0, [Due]-$[LocalNow]))+MINUTE(MAX(0, [Due]-$[LocalNow]))>15
Set Due time so Cleaning Staff can see it:
Set Remaining time for quick overview of tasks:
Done button should be hidden if we have IoT Thing installed for that room.
Configure Done button's action to set Room status to Cleaned:
Test
App should run on your phone, and should show the Rooms, and if Done button is clicked, should soon update the screen with setting that Room to green, and putting it to the end of the list:
Step 4: AWS Honeycode App for ManagerYou can create a new App for Managers, which shows the events in the log, to see if they were compliant. ( You can duplicate Cleaning Staff Tool, and remove unneeded controls. )
Set the data source for the WorkOrdersList to =FILTER(L_Log, "ORDER BY L_Log[Due] DESC"), which shows the items from the L_Log table:
You can add filters for manager UI, like Room title or cleaning staff name, which will enable Managers to filter the result list in the App:
Then add color coding:
- Red : =([Due]-[Cleaned])<0
- Green : =([Due]-[Cleaned])>=0
Test
App should run on your phone, and should show the Rooms and Agents and color coding compliant cleanings to green and non-compliant cleanings to red:
Step 5: Connect AWS account with Honeycode accountAs our data store is in AWS Honeycode, we need to connect AWS account to it using the steps described on this page: Connecting Honeycode to an AWS Account
Test
The below AWS CLI command should list the tables of your Honeycode workbook:
aws honeycode list-tables --workbook-id=ecf97bf3-57d1-48c2-a1e4-2886fe4df3ff
Workbook ID is the guid between %3A and %2F of the url when you edit one of your tables in the workbook in Honeycode:
Clone the code from GitHub (link at the bottom). In the Lambda folder there is a python code which will be executed by IoT Rule if there is a change in the IoT Shadow (when the cleaning staff clicks the Cleaned button on the IoT Thing)
So create a new lambda function in us-west-2 (Oregon) AWS region, called cleaning-tracker-lambda with Python runtime. Oregon is used, because both Honeycode and IoT Core are available in that region. Paste the lambda code to the editor. This code will process the message which will be sent by an IoT rule later, then extract the IoT clientId from it, filter the WorkOrders table using that Id, and then update the Status column of that row to 'Cleaned'.
It has 3 variables defined within the code which should be pre-filled based on your AWS Honeycode Ids:
workbookId = 'ecf97bf3-57d1-48c2-a1e4-2886fe4df3ff' # NOTE: Your Workbook Id in Honeycode
tableId = '654cf941-7739-40fc-a43e-c8609132c9c5' # NOTE: Your WorkOrders Table Id within the Workbook
columnId = 'b983ffd4-61a7-4f89-944b-13916cb13803' # NOTE: The Status column within the WorkOrders table
workbookId is the same as above in Step 5.
tableId can be determined by running AWS CLI command, which will list your tableIds along with tableNames. You will need the WorkOrders table:
aws honeycode list-tables --workbook-id=ecf97bf3-57d1-48c2-a1e4-2886fe4df3ff
---
{
"tables": [
{
"tableId": "654cf941-7739-40fc-a43e-c8609132c9c5",
"tableName": "A_WorkOrders"
},
{
"tableId": "36135fdc-27ed-436a-9237-21c35def5f20",
"tableName": "D_Customers"
},
{
"tableId": "112369af-6ac1-446f-8fd1-0118f8120c74",
"tableName": "D_Properties"
},
{
"tableId": "d25fb00c-3ef9-4f88-9a72-54d962b7c66f",
"tableName": "L_Log"
},
{
"tableId": "070cde25-b179-4462-ab8a-18bed5fc91dd",
"tableName": "M_Status"
},
{
"tableId": "19f86da4-6232-43b2-ab23-9d67be644fb4",
"tableName": "Z_Icons"
}
],
"workbookCursor": 618391759
}
columnId can be determined by below command, which shows 1 row with data and also the columnIds in the beginning of the response, like this. You will need the Status columnId:
aws honeycode list-table-rows --workbook-id=ecf97bf3-57d1-48c2-a1e4-2886fe4df3ff --table-id=654cf941-7739-40fc-a43e-c8609132c9c5 --max-results=1
---
{
"columnIds": [
"9083184e-02d3-4bcb-b957-d139792451f5",
"f8526acf-bc65-4d2c-ab39-755f520fff56",
"b983ffd4-61a7-4f89-944b-13916cb13803",
"d372ed0a-cb8b-4960-8175-505b736c334c",
"bf1404fa-54af-49d2-86ac-ea1d53dd127e",
"ab0a2808-e5e7-4380-9e1d-ed3b6ba92b69",
"2a1106c5-a7de-41cc-92ce-5dfcef59be69",
"1322a4a4-c465-4973-9a52-361a8078068a"
],
...
Once variables are modified, you can click Deploy button, and the lambda code is ready.
Still need to give the Lambda role access to be able to Read and Write Honeycode tables, so let's:
- navigate to Configration / Permissions
- click on cleaning-tracker-lambda-role
- on the IAM Management Console, click Attach policies button
- search for AmazonHoneycodeWorkbookFullAccess policy, and tick the box next to it
- click Attach policy button
Test
Lambda can be tested on AWS if you configure a new Test event and paste the below JSON as content. Make sure to replace the clientidStatus to the IoT clientid.
{
"state": {
"reported": {
"timestampStatus": "2021-08-15 09:38:42",
"clientidStatus": "0123456789abcdef",
"cleaningStatus": "CLEANED"
}
},
"metadata": {
"reported": {
"timestampStatus": {
"timestamp": 1629020386
},
"clientidStatus": {
"timestamp": 1629020386
},
"cleaningStatus": {
"timestamp": 1629020386
}
}
},
"version": 7348,
"timestamp": 1629020386,
"clientToken": "0123456789abcdef-4"
}
You should get a 200 SUCCESS. This should also update the Status of the respective row to Cleaned in WorkOrders table. Then of course our Honeycode event will set it back to Opened, but you still can see the changes in the Due column.
Step 7: Setup IoT Rule to invoke Lambda functionAWS IoT Shadow is a layer on top of MQTT messages. It has its state in AWS and as well as at IoT Thing side in JSON format. Whichever updates any value in it, it will be sent to the other party, so they can react.
Here we want to invoke our Lambda function if the IoT Thing has updated the Shadow.
Navigate to AWS IoT > Act > Rules and create a new rule:
- Name: cleaning_tracker_rule
- Query statement with IoT clientid: SELECT * FROM '$aws/things/0123456789abcdef/shadow/update/accepted'
- Add action: Send a message to a Lambda funcion
- Select cleaning-tracker-lambda
Rule will be connected to Lambda and will be Enabled in some moments. This rule will react to IoT Thing status changes and will call our lambda with the message details.
Finally we have prepared everything for the most important piece which is the code for the IoT Thing. Clone the code from GitHub (link at the bottom). In the IoT folder there is the code written in C what will be needed.
This is built up from the Smart Thermostat example, so here you will find what, how and why was implemented. Our changes can also be followed reading the git commits.
First of all copy your pre-configured sdkconfig file from the Smart Thermostat folder to the IoT folder. This file is not part of our repo (by.gitignore) because it contains WiFi SSID and password as well as the unique AWS url to MQTT queue of your IoT Thing.
Cleanup
Some unneeded files and modules were removed, like FFT.c as we are not processing voice.
ui.c
The following UI methods are created:
// sets wifi label text and state
void ui_wifi_label_update(bool state, char *ssid);
// sets date label based on date value
void ui_date_label_update(rtc_date_t date);
// queries whether the Cleaned button is already clicked & resets its state to false
bool is_cleaned_button_clicked();
// sets the value of the due bar ( 0 .. 100 )
void ui_set_due_bar(int16_t value);
// sets the color of the leds ( example: 0x00FF00 )
void ui_set_led_color(uint32_t color);
Button click event sets a global variable, which can be queried later:
// called if the user clicks the Cleaned button
static void cleaned_button_event_handler(lv_obj_t * obj, lv_event_t event)
{
if(event == LV_EVENT_CLICKED) {
cleaned_button_clicked = true; // store that the button was clicked
ESP_LOGI(TAG, "Done button clicked");
}
}
// queries whether the Cleaned button is already clicked & resets its state to false
bool is_cleaned_button_clicked() {
bool ret = cleaned_button_clicked; // return state
cleaned_button_clicked = false; // set back to current state to false
return ret;
}
cleaned_button = lv_btn_create(lv_scr_act(), NULL);
lv_obj_add_style(cleaned_button, LV_BTN_PART_MAIN, &cleaned_button_style);
lv_obj_set_event_cb(cleaned_button, cleaned_button_event_handler); // handler
lv_obj_set_width(cleaned_button, 200);
lv_obj_align(cleaned_button, NULL, LV_ALIGN_IN_BOTTOM_MID, 0, -20);
Here we are using styles as well, like:
static lv_style_t cleaned_button_style;
lv_style_set_border_color(&cleaned_button_style, LV_STATE_DEFAULT, LV_COLOR_GREEN);
lv_style_set_border_color(&cleaned_button_style, LV_STATE_FOCUSED, LV_COLOR_GREEN);
static lv_style_t title_style; // create title style (big font)
lv_style_init(&title_style);
lv_style_set_text_font(&title_style, LV_STATE_DEFAULT, LV_THEME_DEFAULT_FONT_TITLE);
lv_style_set_text_color(&title_style, LV_STATE_DEFAULT, LV_COLOR_BLACK);
static lv_style_t subtitle_style; // create title style (medium size font)
lv_style_init(&subtitle_style);
lv_style_set_text_font(&subtitle_style, LV_STATE_DEFAULT, LV_THEME_DEFAULT_FONT_SUBTITLE);
lv_style_set_text_color(&subtitle_style, LV_STATE_DEFAULT, LV_COLOR_BLACK);
Important: We also modified font size for the styles, and that can be done in the sdkconfig file, like this:
CONFIG_LV_FONT_MONTSERRAT_32=y
CONFIG_LV_FONT_MONTSERRAT_48=y
# CONFIG_LV_FONT_DEFAULT_SUBTITLE_MONTSERRAT_16 is not set
CONFIG_LV_FONT_DEFAULT_SUBTITLE_MONTSERRAT_32=y
# CONFIG_LV_FONT_DEFAULT_TITLE_MONTSERRAT_16 is not set
CONFIG_LV_FONT_DEFAULT_TITLE_MONTSERRAT_48=y
The below part shows how to print WiFi symbol and how to change text colors within the same text:
// sets wifi label text and state
void ui_wifi_label_update(bool state, char *ssid){
xSemaphoreTake(xGuiSemaphore, portMAX_DELAY);
if (state == false) { // if there is no wifi signal
lv_label_set_text(wifi_label, LV_SYMBOL_WIFI); // black wifi symbol
}
else{
char buffer[100];
sprintf (buffer, "#0000ff %s # %s", LV_SYMBOL_WIFI, ssid);
lv_label_set_text(wifi_label, buffer);
}
xSemaphoreGive(xGuiSemaphore);
LVGL UI documentation can be found here. This shows all available controls, styles and property settings, such as alignment.
And the LEDs are set like this:
// sets the color of the leds ( example: 0x00FF00 )
void ui_set_led_color(uint32_t color) {
Core2ForAWS_Sk6812_SetSideColor(SK6812_SIDE_LEFT, color);
Core2ForAWS_Sk6812_SetSideColor(SK6812_SIDE_RIGHT, color);
Core2ForAWS_Sk6812_Show();
}
main.c
The main file of the IoT project was also cleaned up and our logic was added. Also logging was added to many places and logs to console output instead of UI.
One of the changes made is that we are not rebooting if the IoT Thing was not able to connect to the shadow. Sometimes we had timeout issues, and Thing restart took much time.
// Connect to shadow in infinite loop until connected successfully
while(true) {
ESP_LOGI(TAG, "Shadow Connect");
rc = aws_iot_shadow_connect(&iotCoreClient, &scp);
if(SUCCESS != rc) {
ESP_LOGE(TAG, "aws_iot_shadow_connect returned error %d, retrying...", rc);
} else {
ESP_LOGI(TAG, "Connected to AWS IoT Device Shadow service");
break; // exit from the loop
}
}
When the Thing is connected to WiFi and IoT Shadow, we set the Due time for current time + 1 hour on the Thing as well:
// initialize cleaning due date to current time + 1 hour
BM8563_GetTime(&dueDate);
dueDate.hour+=1;
Then we start an infinite loop, which gets time and show time on UI, calculates remaining time, sets LEDs based on the difference, and also sets the progressbar (which will not go below zero):
// START get sensor readings + update UI
BM8563_GetTime(&date); // get current date time
ui_date_label_update(date); // show time on UI
// minutes between now and cleaning due time
int timediff = (dueDate.hour * 60 + dueDate.minute) - (date.hour * 60 + date.minute); ESP_LOGI(TAG, "timediff: %d", timediff);
if (timediff < 0)
ui_set_led_color(0xFF0000); // set LED strips to RED if no time left
else if (timediff < 15)
ui_set_led_color(0xFFFF00); // set LED strips to YELLOW if 15 or less mins left
else
ui_set_led_color(0x00FF00); // set LED strips to GREEN otherwise
if (timediff < 0)
timediff = 0;
ui_set_due_bar(timediff * 100 / 60); // show remaining time on the progressbar as well
// END get sensor readings
We check if the button was clicked in the meantime, and if yes, then we increase the due time to +1 hour. Also a JSON message should be constructed, and IoT Shadow should be updated. To save costs we update the IoT Shadow only if that is really needed (when 'Cleaned' button clicked):
// if room is cleaned
if (is_cleaned_button_clicked()) { // send message only if Cleaned
BM8563_GetTime(&dueDate);
dueDate.hour+=1; // update cleaning due date to current time + 1 hour
// set values for shadow document
sprintf(timestampStatus, "%d-%02d-%02d %02d:%02d:%02d", date.year, date.month, date.day, date.hour, date.minute, date.second); // date time stamp
sprintf(clientidStatus, "%s", client_id); // IoT id
sprintf(cleaningStatus, "CLEANED"); // Cleaning status
// log
ESP_LOGI(TAG, "****************************************************************");
ESP_LOGI(TAG, "On Device: timestampStatus %s", timestampStatus);
ESP_LOGI(TAG, "On Device: clientidStatus %s", clientidStatus);
ESP_LOGI(TAG, "On Device: cleaningStatus %s", cleaningStatus);
// compose and update shadow document with: timestamp + clientid + cleaningstatus
rc = aws_iot_shadow_init_json_document(JsonDocumentBuffer,
sizeOfJsonDocumentBuffer);
if(SUCCESS == rc) {
rc = aws_iot_shadow_add_reported(JsonDocumentBuffer,
sizeOfJsonDocumentBuffer, 3,
×tampStatusActuator,
&clientidStatusActuator,
&cleaningStatusActuator);
if(SUCCESS == rc) {
rc = aws_iot_finalize_json_document(JsonDocumentBuffer,
sizeOfJsonDocumentBuffer);
if(SUCCESS == rc) {
ESP_LOGI(TAG, "Update Shadow: %s", JsonDocumentBuffer);
rc = aws_iot_shadow_update(&iotCoreClient, client_id,
JsonDocumentBuffer, ShadowUpdateStatusCallback, NULL, 4, true);
shadowUpdateInProgress = true;
}
}
}
ESP_LOGI(TAG, "****************************************************************");
}
Then we wait 1 sec and the infinite loop continues.
vTaskDelay(pdMS_TO_TICKS(1000)); // wait 1 sec, then loop
And that's it !
Remark: cleaningStatus_Callback method is currently empty. In a later enhancement it can be used to e.g. update the Due date from AWS, as this gets called whenever the IoT Shadow is changed at AWS side.
Test
Let's issue below command in PlatformIO Terminal:
pio run --environment core2foraws --target upload --target monitor
It will compile project, create firmware and upload it to the IoT Thing. Thing will restart and execute code, while you can see its console log in the PlatformIO Terminal.
Some important milestones in the log are:
clientId:
␛[0;32mI (3425) MAIN: Device client Id: >> 0123456789abcde01 <<␛[0m
WiFi connection:
␛[0;32mI (3235) WIFI: Setting Wi-Fi configuration to SSID: ultrix␛[0m
␛[0;32mI (9135) WIFI: Wi-Fi connected. Device IP address: 192.168.0.213␛[0m
Connecting to AWS IoT Shadow:
␛[0;32mI (9135) MAIN: Shadow Init␛[0m
␛[0;32mI (9135) MAIN: Shadow Connect␛[0m
␛[0;32mI (16265) MAIN: Connected to AWS IoT Device Shadow service␛[0m
Loop displaying the remaining time:
␛[0;32mI (16765) MAIN: timediff: 60␛[0m
:
:
␛[0;32mI (16765) MAIN: timediff: 59␛[0m
Once 'Cleaned' button was clicked, these logs will be displayed:
␛[0;32mI (31945) UI: Done button clicked␛[0m
␛[0;32mI (32365) MAIN: *****************************************************************************************␛[0m
␛[0;32mI (32365) MAIN: On Device: timestampStatus 2021-09-15 17:24:56␛[0m
␛[0;32mI (32375) MAIN: On Device: clientidStatus 0123456789abcde01 ␛[0m
␛[0;32mI (32385) MAIN: On Device: cleaningStatus CLEANED␛[0m
␛[0;32mI (32385) MAIN: Update Shadow: {"state":{"reported":{"timestampStatus":"2021-09-15 17:24:56","clientidStatus":"0123456789abcde01 ","cleaningStatus":"CLEANED"}}, "clientToken":"0123456789abcde01 -0"}␛[0m
␛[0;32mI (35065) MAIN: *****************************************************************************************␛[0m
␛[0;32mI (35065) MAIN: Stack remaining for task 'aws_iot_task' is 2044 bytes␛[0m
␛[0;32mI (36095) MAIN: Update accepted␛[0m
The same JSON is visible on AWS Console as well:
After booting, LEDS will be blue on the IoT Thing. Soon it will connect to WiFi, and its SSID will be shown in the top left corner. Some time later it will connect to the AWS IoT Shadow, and loop will start, so: the current time will be shown, LEDs will be green, and the progressbar will show the percentage of remaining time staring from 1 hour. If only 15 minutes left, LEDs will be yellow. If all time left, LEDs will be red. Whenever you push the 'Cleaned' button, 1 hour countdown starts again, the progressbar should reset to 100%, the LEDs should be green again, and a message is also sent through AWS IoT Shadow, which will trigger the IoT Rule, and so on.
Future possibilitiesAggregate data for compliance report.
Connect the system to corporate AD, so:
- IoT Thing can show next meeting name and start time on its UI
- system can plan cleaning cycles when there is no meeting in the room
Comments