├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── bqschema.json ├── case_extended.FCStd ├── cloud_function ├── function.go ├── go.mod ├── go.sum ├── locations.go ├── sensor.pb.go └── sensor.proto ├── contributing.md └── firmware ├── CMakeLists.txt ├── Makefile ├── components ├── google-mqtt │ ├── CMakeLists.txt │ ├── Kconfig │ ├── component.mk │ ├── google-cloud-iot-arduino │ ├── google-mqtt.cc │ ├── google-mqtt.h │ ├── mqtt │ ├── sensor.pb-c.c │ ├── sensor.pb-c.h │ └── sensor.proto ├── sensor-bmp280 │ ├── CMakeLists.txt │ ├── adafruit │ ├── sensor_bmp280.cc │ └── sensor_bmp280.h ├── sensor-scd30 │ ├── CMakeLists.txt │ ├── sensor_scd30.cc │ ├── sensor_scd30.h │ └── sparkfun ├── sensor-sgp30 │ ├── CMakeLists.txt │ ├── sensor_sgp30.cc │ ├── sensor_sgp30.h │ └── sparkfun ├── sensor-smuart04l │ ├── CMakeLists.txt │ ├── sensor_smuart04l.cc │ └── sensor_smuart04l.h └── sensor-wrapper │ ├── CMakeLists.txt │ ├── sensor_types.h │ ├── sensor_wrapper.cc │ └── sensor_wrapper.h ├── googleca.pem ├── main ├── CMakeLists.txt ├── component.mk └── main.cc ├── makemac.py ├── partition-table.csv └── provision.sh /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.old 3 | .vscode/ 4 | keys/ 5 | spiffs/ 6 | CMakeCache.txt 7 | CMakeFiles/ 8 | spiffs.bin 9 | *_key.* 10 | firmware/sdkconfig 11 | 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/arduino_mqtt"] 2 | path = third_party/arduino_mqtt 3 | url = https://github.com/256dpi/arduino-mqtt 4 | [submodule "third_party/google_cloud_iot_arduino"] 5 | path = third_party/google_cloud_iot_arduino 6 | url = https://github.com/GoogleCloudPlatform/google-cloud-iot-arduino 7 | [submodule "third_party/adafruit_bmp280"] 8 | path = third_party/adafruit_bmp280 9 | url = https://github.com/adafruit/Adafruit_BMP280_Library 10 | [submodule "third_party/sparkfun_sgp30"] 11 | path = third_party/sparkfun_sgp30 12 | url = https://github.com/sparkfun/SparkFun_SGP30_Arduino_Library 13 | [submodule "third_party/sparkfun_scd30"] 14 | path = third_party/sparkfun_scd30 15 | url = https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library 16 | [submodule "third_party/arduino_esp32"] 17 | path = firmware/components/arduino 18 | url = https://github.com/espressif/arduino-esp32 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project demonstrates how an ESP32 can be combined with Google Cloud IoT, Google Cloud Functions, Google StackDriver and Google BigQuery to collect and analyze metrics from environmental sensors. 2 | 3 | **This example is not an officially supported Google product, does not have a SLA/SLO, and should not be used in production.** 4 | 5 | The project consists of three components: 6 | 7 | - The Google Cloud Function - This is a simple Go-based piece of code. Simply configure the Cloud Function in the console to be triggered on a telemetry event from the IoT project. 8 | - The firmware - This is an ESP-IDF project and must be built using the ESP-IDF toolset. 9 | - A CAD model for the PCB design in [this repository](https://github.com/bobobo1618/iot-environment-sensor). 10 | 11 | # Instructions 12 | 13 | There are three things that have to be set up: 14 | 15 | - The hardware 16 | - The firmware 17 | - The Google Cloud Platform project which will collect metrics for analysis 18 | 19 | ## Hardware 20 | 21 | This has been tested with the PCB design from [this repository](https://github.com/bobobo1618/iot-environment-sensor). The board is intended to be combined with: 22 | 23 | - A Sensirion SCD30 CO2 sensor, with a pin-header soldered to it. 24 | - An Amphenol SM-UART-04L particulate sensor. 25 | - An ESP32-PICO-KIT. 26 | 27 | The board can be manufactured manually or by a PCB fab. 28 | 29 | This repository also contains a FreeCAD model for a case intended to fit the PCB design. 30 | 31 | ## Google Cloud Platform 32 | 33 | The project is intended to be used with: 34 | 35 | - IoT Core: To act as a bridge between the MQTT protocol used by the sensors and Pubsub. 36 | - Pubsub: To act as a bridge between IoT Core and Cloud Functions. 37 | - StackDriver: For real-time monitoring and alerting on sensor readings. 38 | - BigQuery: For long-term historical data collection and analysis. 39 | - Cloud Functions: To ingest incoming data and store it in BigQuery and/or StackDriver. 40 | 41 | The recommended setup procedure is (using the [Cloud SDK](https://cloud.google.com/sdk) CLI): 42 | 43 | - Create a GCP project ([docs](https://cloud.google.com/resource-manager/docs/creating-managing-projects)): `gcloud projects create ` 44 | - Create an IoT Core registry in the region of your choice ([docs](https://cloud.google.com/iot/docs/how-tos/devices)): `gcloud iot registries create --project= --region=` 45 | - Create the partitioned BigQuery table where your historical data will be stored ([docs](https://cloud.google.com/bigquery/docs/creating-column-partitions)): `bq mk --table --schema bqschema.json --time_partitioning_field Timestamp :.` 46 | - Set up the Cloud Functions ([docs](https://cloud.google.com/functions/docs/deploying/filesystem)): 47 | - BigQuery: 48 | ```bash 49 | gcloud functions deploy \ 50 | --runtime go111 \ 51 | --trigger-topic telemetry \ 52 | --entry-point GasTranslator \ 53 | --memory 128MB \ 54 | --source cloud_function \ 55 | --set-env-vars=PROJECT_ID=,BQ_DATASET=,BQ_TABLE= 56 | ``` 57 | - StackDriver: 58 | ```bash 59 | gcloud functions deploy \ 60 | --runtime go111 \ 61 | --trigger-topic telemetry \ 62 | --entry-point GasTranslatorStackdriver \ 63 | --memory 128MB \ 64 | --source cloud_function \ 65 | --set-env-vars=PROJECT_ID=,SD_NAMESPACE=,SD_LOCATION=, 66 | ``` 67 | 68 | ## Firmware 69 | 70 | The firmware is an [Espressif ESP-IDF](https://docs.espressif.com/projects/esp-idf/en/latest/) project and in particular uses [arduino-esp32](https://github.com/espressif/arduino-esp32). As such, it depends on version 3.3.1 of ESP-IDF which can be installed following [these instructions](https://docs.espressif.com/projects/esp-idf/en/v3.3.1/get-started/index.html). arduino-esp32 frequently updates its IDF version so check the git log for the most recent commit hash: 71 | 72 | ```bash 73 | cd firmware/components/arduino 74 | git log --oneline --grep 'IDF' firmware/components/arduino/ 75 | ``` 76 | 77 | Once you've set up ESP-IDF, you'll need to configure the project by running `idf.py menuconfig` from the firmware directory. In particular, under `Component Config -> Google IoT Configuration` there are options for the IoT Core project and registry, as well as the WiFi AP and password. 78 | 79 | The firmware is also built with support for OTA firmware updates and requires that you generate an OTA signing key. To do so: 80 | 81 | - Generate a private key: `espsecure.py generate_signing_key secure_boot_signing_key.pem` 82 | - Generate a public key: `espsecure.py extract_public_key --keyfile secure_boot_signing_key.pem signature_verification_key.bin` 83 | 84 | The next step is to build the firmware with `idf.py build`. 85 | 86 | Finally, you need to flash the device and provision if for IoT Core. The `provision.sh` script does both. 87 | 88 | ### OTAs 89 | 90 | The firmware assumes that the firmware update will be stored using Google Cloud Storage. If you want to store it elsewhere, you'll need to replace `googleca.pem` with the public certificate of the HTTPS server you intend to update from. 91 | 92 | In order to send an OTA update, you must: 93 | 94 | - Build: `idf.py build` 95 | - Sign: `espsecure.py sign_data --keyfile secure_boot_signing_key.pem build/hello-world.bin` 96 | - Upload to your HTTPS server: `gsutil cp -a public-read build/hello-world.bin gs:///firmware.bin` 97 | - Send the update command using IoT Core: 98 | ``` 99 | gcloud iot devices commands send \ 100 | --device= \ 101 | --project= \ 102 | --region= \ 103 | --registry= \ 104 | --command-data='update: https://storage.googleapis.com//firmware.bin' 105 | ``` 106 | -------------------------------------------------------------------------------- /bqschema.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mode": "NULLABLE", 4 | "name": "Timestamp", 5 | "type": "TIMESTAMP" 6 | }, 7 | { 8 | "mode": "NULLABLE", 9 | "name": "DeviceId", 10 | "type": "STRING" 11 | }, 12 | { 13 | "fields": [ 14 | { 15 | "name": "Temperature", 16 | "type": "FLOAT" 17 | }, 18 | { 19 | "name": "Pressure", 20 | "type": "FLOAT" 21 | } 22 | ], 23 | "mode": "NULLABLE", 24 | "name": "BMP280", 25 | "type": "RECORD" 26 | }, 27 | { 28 | "fields": [ 29 | { 30 | "name": "Temperature", 31 | "type": "FLOAT" 32 | }, 33 | { 34 | "name": "Humidity", 35 | "type": "FLOAT" 36 | }, 37 | { 38 | "name": "CO2", 39 | "type": "FLOAT" 40 | } 41 | ], 42 | "mode": "NULLABLE", 43 | "name": "SCD30", 44 | "type": "RECORD" 45 | }, 46 | { 47 | "fields": [ 48 | { 49 | "name": "CO2", 50 | "type": "INTEGER" 51 | }, 52 | { 53 | "name": "TVOC", 54 | "type": "INTEGER" 55 | }, 56 | { 57 | "name": "BaselineCO2", 58 | "type": "INTEGER" 59 | }, 60 | { 61 | "name": "BaselineTVOC", 62 | "type": "INTEGER" 63 | }, 64 | { 65 | "name": "H2", 66 | "type": "INTEGER" 67 | }, 68 | { 69 | "name": "Ethanol", 70 | "type": "INTEGER" 71 | } 72 | ], 73 | "mode": "NULLABLE", 74 | "name": "SGP30", 75 | "type": "RECORD" 76 | }, 77 | { 78 | "fields": [ 79 | { 80 | "name": "PM10Smoke", 81 | "type": "INTEGER" 82 | }, 83 | { 84 | "name": "PM25Smoke", 85 | "type": "INTEGER" 86 | }, 87 | { 88 | "name": "PM100Smoke", 89 | "type": "INTEGER" 90 | }, 91 | { 92 | "name": "PM10Env", 93 | "type": "INTEGER" 94 | }, 95 | { 96 | "name": "PM25Env", 97 | "type": "INTEGER" 98 | }, 99 | { 100 | "name": "PM100Env", 101 | "type": "INTEGER" 102 | } 103 | ], 104 | "mode": "NULLABLE", 105 | "name": "SMUART04L", 106 | "type": "RECORD" 107 | }, 108 | { 109 | "mode": "NULLABLE", 110 | "name": "PhysicalLocation", 111 | "type": "STRING" 112 | } 113 | ] 114 | -------------------------------------------------------------------------------- /case_extended.FCStd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/iot-environment-sensors/34ece839e73afe0aa6a91994cf8d90f4b40878bb/case_extended.FCStd -------------------------------------------------------------------------------- /cloud_function/function.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cloud_function 16 | 17 | import ( 18 | "cloud.google.com/go/bigquery" 19 | monitoring "cloud.google.com/go/monitoring/apiv3" 20 | "context" 21 | "fmt" 22 | "github.com/golang/protobuf/proto" 23 | timestamp "github.com/golang/protobuf/ptypes/timestamp" 24 | metricpb "google.golang.org/genproto/googleapis/api/metric" 25 | monitoredres "google.golang.org/genproto/googleapis/api/monitoredres" 26 | monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3" 27 | "log" 28 | "os" 29 | "time" 30 | ) 31 | 32 | type PubSubMessage struct { 33 | Data []byte `json:"data"` 34 | Attributes map[string]string 35 | } 36 | 37 | type BMP280 struct { 38 | Temperature float32 39 | Pressure float32 40 | } 41 | 42 | func (n *BMP280) FromProto(proto *BMP280Reading) { 43 | n.Temperature = *proto.Temperature 44 | n.Pressure = *proto.Pressure 45 | } 46 | 47 | type SCD30 struct { 48 | Temperature float32 49 | Humidity float32 50 | CO2 float32 51 | } 52 | 53 | func (n *SCD30) FromProto(proto *SCD30Reading) { 54 | n.Temperature = *proto.Temperature 55 | n.Humidity = *proto.Humidity 56 | n.CO2 = *proto.Co2 57 | } 58 | 59 | type SGP30 struct { 60 | CO2 uint32 61 | TVOC uint32 62 | BaselineCO2 uint32 63 | BaselineTVOC uint32 64 | H2 uint32 65 | Ethanol uint32 66 | } 67 | 68 | func (n *SGP30) FromProto(proto *SGP30Reading) { 69 | n.CO2 = *proto.Co2 70 | n.TVOC = *proto.Tvoc 71 | n.BaselineCO2 = *proto.BaselineCo2 72 | n.BaselineTVOC = *proto.BaselineTvoc 73 | n.H2 = *proto.H2 74 | n.Ethanol = *proto.Ethanol 75 | } 76 | 77 | type SMUART04L struct { 78 | PM10Smoke uint32 79 | PM25Smoke uint32 80 | PM100Smoke uint32 81 | PM10Env uint32 82 | PM25Env uint32 83 | PM100Env uint32 84 | } 85 | 86 | func (n *SMUART04L) FromProto(proto *SMUART04LReading) { 87 | n.PM10Smoke = *proto.Pm10Smoke 88 | n.PM25Smoke = *proto.Pm25Smoke 89 | n.PM100Smoke = *proto.Pm100Smoke 90 | n.PM10Env = *proto.Pm10Env 91 | n.PM25Env = *proto.Pm25Env 92 | n.PM100Env = *proto.Pm100Env 93 | } 94 | 95 | type BQRow struct { 96 | Timestamp time.Time 97 | DeviceId string 98 | BMP280 BMP280 99 | SCD30 SCD30 100 | SGP30 SGP30 101 | SMUART04L SMUART04L 102 | PhysicalLocation string 103 | } 104 | 105 | func (n *BQRow) FromUpdate(update *SensorUpdate, deviceId string) { 106 | n.Timestamp = time.Unix(int64(*update.Timestamp), 0) 107 | n.DeviceId = deviceId 108 | n.BMP280.FromProto(update.Bmp280) 109 | n.SCD30.FromProto(update.Scd30) 110 | n.SGP30.FromProto(update.Sgp30) 111 | n.SMUART04L.FromProto(update.Smuart04L) 112 | location, ok := LocationMap[deviceId] 113 | if !ok { 114 | location = "UnknownLocation" 115 | } 116 | n.PhysicalLocation = location 117 | } 118 | 119 | func GasTranslator(ctx context.Context, m PubSubMessage) error { 120 | log.Printf("Received message %v\n", m) 121 | update := &SensorUpdate{} 122 | err := proto.Unmarshal(m.Data, update) 123 | if err != nil { 124 | log.Printf("Parse failed: %v.\n", err) 125 | return err 126 | } 127 | 128 | bqrow := BQRow{} 129 | bqrow.FromUpdate(update, m.Attributes["deviceId"]) 130 | log.Printf("Received update %v\n", bqrow) 131 | 132 | client, err := bigquery.NewClient(ctx, os.Getenv("PROJECT_ID")) 133 | if err != nil { 134 | log.Printf("Failed to set up BigQuery client: %s\n", err) 135 | return err 136 | } 137 | 138 | u := client.Dataset(os.Getenv("BQ_DATASET")).Table(os.Getenv("BQ_TABLE")).Inserter() 139 | 140 | items := []*BQRow{&bqrow} 141 | 142 | if err := u.Put(ctx, items); err != nil { 143 | log.Printf("Failed to insert to BigQuery: %s\n", err) 144 | return err 145 | } 146 | 147 | return nil 148 | } 149 | func ReportDoubleMetric(ctx context.Context, c *monitoring.MetricClient, ts *timestamp.Timestamp, m PubSubMessage, metricType string, metricLabels map[string]string, value float64) error { 150 | req := &monitoringpb.CreateTimeSeriesRequest{ 151 | Name: fmt.Sprintf("projects/%s", os.Getenv("PROJECT_ID")), 152 | TimeSeries: []*monitoringpb.TimeSeries{{ 153 | Metric: &metricpb.Metric{ 154 | Type: metricType, 155 | Labels: metricLabels, 156 | }, 157 | Resource: &monitoredres.MonitoredResource{ 158 | Type: "generic_node", 159 | Labels: map[string]string{ 160 | "node_id": m.Attributes["deviceId"], 161 | "namespace": os.Getenv("SD_NAMESPACE"), 162 | "location": os.Getenv("SD_LOCATION"), 163 | }, 164 | }, 165 | Points: []*monitoringpb.Point{ 166 | { 167 | Interval: &monitoringpb.TimeInterval{ 168 | StartTime: ts, 169 | EndTime: ts, 170 | }, 171 | Value: &monitoringpb.TypedValue{ 172 | Value: &monitoringpb.TypedValue_DoubleValue{ 173 | DoubleValue: value, 174 | }, 175 | }, 176 | }, 177 | }, 178 | }}, 179 | } 180 | log.Printf("writeTimeseriesRequest: %+v\n", req) 181 | 182 | err := c.CreateTimeSeries(ctx, req) 183 | if err != nil { 184 | return fmt.Errorf("could not write time series value, %v ", err) 185 | } 186 | return nil 187 | } 188 | 189 | func ReportIntMetric(ctx context.Context, c *monitoring.MetricClient, ts *timestamp.Timestamp, m PubSubMessage, metricType string, metricLabels map[string]string, value int64) error { 190 | req := &monitoringpb.CreateTimeSeriesRequest{ 191 | Name: fmt.Sprintf("projects/%s", os.Getenv("PROJECT_ID")), 192 | TimeSeries: []*monitoringpb.TimeSeries{{ 193 | Metric: &metricpb.Metric{ 194 | Type: metricType, 195 | Labels: metricLabels, 196 | }, 197 | Resource: &monitoredres.MonitoredResource{ 198 | Type: "generic_node", 199 | Labels: map[string]string{ 200 | "node_id": m.Attributes["deviceId"], 201 | "namespace": os.Getenv("SD_NAMESPACE"), 202 | "location": os.Getenv("SD_LOCATION"), 203 | }, 204 | }, 205 | Points: []*monitoringpb.Point{ 206 | { 207 | Interval: &monitoringpb.TimeInterval{ 208 | StartTime: ts, 209 | EndTime: ts, 210 | }, 211 | Value: &monitoringpb.TypedValue{ 212 | Value: &monitoringpb.TypedValue_Int64Value{ 213 | Int64Value: value, 214 | }, 215 | }, 216 | }, 217 | }, 218 | }}, 219 | } 220 | log.Printf("writeTimeseriesRequest: %+v\n", req) 221 | 222 | err := c.CreateTimeSeries(ctx, req) 223 | if err != nil { 224 | return fmt.Errorf("could not write time series value, %v ", err) 225 | } 226 | return nil 227 | } 228 | 229 | func GasTranslatorStackdriver(ctx context.Context, m PubSubMessage) error { 230 | log.Printf("Received message %v\n", m) 231 | update := &SensorUpdate{} 232 | err := proto.Unmarshal(m.Data, update) 233 | if err != nil { 234 | log.Printf("Parse failed: %v.\n", err) 235 | return err 236 | } 237 | 238 | c, err := monitoring.NewMetricClient(ctx) 239 | if err != nil { 240 | return err 241 | } 242 | 243 | location, ok := LocationMap[m.Attributes["deviceId"]] 244 | if !ok { 245 | location = "UnknownLocation" 246 | } 247 | 248 | ts := ×tamp.Timestamp{ 249 | Seconds: int64(*update.Timestamp), 250 | } 251 | if err := ReportDoubleMetric(ctx, c, ts, m, "custom.googleapis.com/co2", map[string]string{"physical_location": location, "sensor": "scd30"}, float64(*update.Scd30.Co2)); err != nil { 252 | return err 253 | } 254 | if err := ReportDoubleMetric(ctx, c, ts, m, "custom.googleapis.com/humidity", map[string]string{"physical_location": location, "sensor": "scd30"}, float64(*update.Scd30.Humidity)); err != nil { 255 | return err 256 | } 257 | if err := ReportDoubleMetric(ctx, c, ts, m, "custom.googleapis.com/temperature", map[string]string{"physical_location": location, "sensor": "scd30"}, float64(*update.Scd30.Temperature)); err != nil { 258 | return err 259 | } 260 | if err := ReportDoubleMetric(ctx, c, ts, m, "custom.googleapis.com/pressure", map[string]string{"physical_location": location, "sensor": "bmp280"}, float64(*update.Bmp280.Pressure)); err != nil { 261 | return err 262 | } 263 | if err := ReportDoubleMetric(ctx, c, ts, m, "custom.googleapis.com/temperature", map[string]string{"physical_location": location, "sensor": "bmp280"}, float64(*update.Bmp280.Temperature)); err != nil { 264 | return err 265 | } 266 | if err := ReportDoubleMetric(ctx, c, ts, m, "custom.googleapis.com/co2", map[string]string{"physical_location": location, "sensor": "sgp30"}, float64(*update.Sgp30.Co2)); err != nil { 267 | return err 268 | } 269 | if err := ReportIntMetric(ctx, c, ts, m, "custom.googleapis.com/tvoc", map[string]string{"physical_location": location, "sensor": "sgp30"}, int64(*update.Sgp30.Tvoc)); err != nil { 270 | return err 271 | } 272 | if err := ReportIntMetric(ctx, c, ts, m, "custom.googleapis.com/baseline_co2", map[string]string{"physical_location": location, "sensor": "sgp30"}, int64(*update.Sgp30.BaselineCo2)); err != nil { 273 | return err 274 | } 275 | if err := ReportIntMetric(ctx, c, ts, m, "custom.googleapis.com/baseline_tvoc", map[string]string{"physical_location": location, "sensor": "sgp30"}, int64(*update.Sgp30.BaselineTvoc)); err != nil { 276 | return err 277 | } 278 | if err := ReportIntMetric(ctx, c, ts, m, "custom.googleapis.com/h2", map[string]string{"physical_location": location, "sensor": "sgp30"}, int64(*update.Sgp30.H2)); err != nil { 279 | return err 280 | } 281 | if err := ReportIntMetric(ctx, c, ts, m, "custom.googleapis.com/ethanol", map[string]string{"physical_location": location, "sensor": "sgp30"}, int64(*update.Sgp30.Ethanol)); err != nil { 282 | return err 283 | } 284 | if err := ReportIntMetric(ctx, c, ts, m, "custom.googleapis.com/particulate", map[string]string{"physical_location": location, "sensor": "sm-uart-04l", "type": "smoke", "granularity": "1.0"}, int64(*update.Smuart04L.Pm10Smoke)); err != nil { 285 | return err 286 | } 287 | if err := ReportIntMetric(ctx, c, ts, m, "custom.googleapis.com/particulate", map[string]string{"physical_location": location, "sensor": "sm-uart-04l", "type": "smoke", "granularity": "2.5"}, int64(*update.Smuart04L.Pm25Smoke)); err != nil { 288 | return err 289 | } 290 | if err := ReportIntMetric(ctx, c, ts, m, "custom.googleapis.com/particulate", map[string]string{"physical_location": location, "sensor": "sm-uart-04l", "type": "smoke", "granularity": "10.0"}, int64(*update.Smuart04L.Pm100Smoke)); err != nil { 291 | return err 292 | } 293 | if err := ReportIntMetric(ctx, c, ts, m, "custom.googleapis.com/particulate", map[string]string{"physical_location": location, "sensor": "sm-uart-04l", "type": "env", "granularity": "1.0"}, int64(*update.Smuart04L.Pm10Env)); err != nil { 294 | return err 295 | } 296 | if err := ReportIntMetric(ctx, c, ts, m, "custom.googleapis.com/particulate", map[string]string{"physical_location": location, "sensor": "sm-uart-04l", "type": "env", "granularity": "2.5"}, int64(*update.Smuart04L.Pm25Env)); err != nil { 297 | return err 298 | } 299 | if err := ReportIntMetric(ctx, c, ts, m, "custom.googleapis.com/particulate", map[string]string{"physical_location": location, "sensor": "sm-uart-04l", "type": "env", "granularity": "10.0"}, int64(*update.Smuart04L.Pm100Env)); err != nil { 300 | return err 301 | } 302 | return nil 303 | } 304 | -------------------------------------------------------------------------------- /cloud_function/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/iot-environment-sensors/cloud_function 2 | 3 | go 1.13 4 | 5 | require ( 6 | cloud.google.com/go v0.61.0 7 | cloud.google.com/go/bigquery v1.9.0 8 | github.com/golang/protobuf v1.4.2 9 | google.golang.org/genproto v0.0.0-20200715011427-11fb19a81f2c 10 | ) 11 | -------------------------------------------------------------------------------- /cloud_function/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.60.0/go.mod h1:yw2G51M9IfRboUH61Us8GqCeF1PzPblB823Mn2q2eAU= 15 | cloud.google.com/go v0.61.0 h1:NLQf5e1OMspfNT1RAHOB3ublr1TW3YTXO8OiWwVjK2U= 16 | cloud.google.com/go v0.61.0/go.mod h1:XukKJg4Y7QsUu0Hxg3qQKUWR4VuWivmyMK2+rUyxAqw= 17 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 18 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 19 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 20 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 21 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 22 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 23 | cloud.google.com/go/bigquery v1.9.0 h1:Izm76KmIoARyQ89CVwEazD5zhH7xtuxkDlnCyoRXrhY= 24 | cloud.google.com/go/bigquery v1.9.0/go.mod h1:JMGKDcmBZE48Feu6ZQ4qUaQDPpD6q6i0N8qVALoMAcc= 25 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 26 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 27 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 28 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 29 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 30 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 31 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 32 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 33 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 34 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 35 | cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 38 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 39 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 40 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 41 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 42 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 43 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 44 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 45 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 46 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 47 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 48 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 49 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 50 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 51 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 52 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 53 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 54 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 55 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 56 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 57 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= 58 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 59 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 60 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 61 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 62 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 63 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 64 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 65 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 66 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 67 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 68 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 69 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 70 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 71 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 72 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 73 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 74 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 75 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 76 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 77 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 78 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 79 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 80 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 81 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 82 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 83 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 84 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 85 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 86 | github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= 87 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 88 | github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= 89 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 90 | github.com/google/martian/v3 v3.0.0 h1:pMen7vLs8nvgEYhywH3KDWJIJTeEr2ULsVWHWYHQyBs= 91 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 92 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 93 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 94 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 95 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 96 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 97 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 98 | github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 99 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 100 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 101 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 102 | github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= 103 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 104 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 105 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 106 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 107 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 108 | github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= 109 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 110 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 111 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 112 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 113 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 114 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 115 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 116 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 117 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 118 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 119 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 120 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 121 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 122 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 123 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 124 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 125 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 126 | go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= 127 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 128 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 129 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 130 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 131 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 132 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 133 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 134 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 135 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 136 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 137 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 138 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 139 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 140 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 141 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 142 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 143 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 144 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 145 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 146 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 147 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 148 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 149 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 150 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 151 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 152 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 153 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 154 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= 155 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 156 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 157 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 158 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 159 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 160 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 161 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 162 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 163 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 164 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 165 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 166 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 167 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 168 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 169 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 170 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 171 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 172 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 173 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 174 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 175 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 176 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 177 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 178 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 179 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 180 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 181 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 182 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 183 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 184 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 185 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 186 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 187 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 188 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 189 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgNrpq9mjcfDemuexIKsU= 190 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 191 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 192 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 193 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 194 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 195 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= 196 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 197 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 198 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 199 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 200 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 201 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 202 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 203 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 204 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 205 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 206 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 207 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 208 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 209 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 210 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 211 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 212 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 213 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 214 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 215 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 216 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 217 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 218 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 219 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 220 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 221 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 222 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 223 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 224 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 225 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 226 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 227 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 228 | golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o= 229 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 230 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 231 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 232 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 233 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 234 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 235 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 236 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 237 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 238 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 239 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 240 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 241 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 242 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 243 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 244 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 245 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 246 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 247 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 248 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 249 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 250 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 251 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 252 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 253 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 254 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 255 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 256 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 257 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 258 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 259 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 260 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 261 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 262 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 263 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 264 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 265 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 266 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 267 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 268 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 269 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 270 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 271 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 272 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 273 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 274 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 275 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 276 | golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 277 | golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed h1:+qzWo37K31KxduIYaBeMqJ8MUOyTayOQKpH9aDPLMSY= 278 | golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 279 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 280 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 281 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 282 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 283 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 284 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 285 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 286 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 287 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 288 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 289 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 290 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 291 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 292 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 293 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 294 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 295 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 296 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 297 | google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk= 298 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 299 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 300 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 301 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 302 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 303 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 304 | google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= 305 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 306 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 307 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 308 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 309 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 310 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 311 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 312 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 313 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 314 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 315 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 316 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 317 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 318 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 319 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 320 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 321 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 322 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 323 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 324 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 325 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 326 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 327 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 328 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 329 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 330 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 331 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 332 | google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 333 | google.golang.org/genproto v0.0.0-20200711021454-869866162049/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 334 | google.golang.org/genproto v0.0.0-20200715011427-11fb19a81f2c h1:6DWnZZ6EY/59QRRQttZKiktVL23UuQYs7uy75MhhLRM= 335 | google.golang.org/genproto v0.0.0-20200715011427-11fb19a81f2c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 336 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 337 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 338 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 339 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 340 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 341 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 342 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 343 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 344 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 345 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 346 | google.golang.org/grpc v1.30.0 h1:M5a8xTlYTxwMn5ZFkwhRabsygDY5G8TYLyQDBxJNAxE= 347 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 348 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 349 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 350 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 351 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 352 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 353 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 354 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 355 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 356 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 357 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 358 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 359 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 360 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 361 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 362 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 363 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 364 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 365 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 366 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 367 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 368 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 369 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 370 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 371 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 372 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 373 | -------------------------------------------------------------------------------- /cloud_function/locations.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cloud_function 16 | 17 | var LocationMap = map[string]string{} -------------------------------------------------------------------------------- /cloud_function/sensor.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Code generated by protoc-gen-go. DO NOT EDIT. 16 | // source: sensor.proto 17 | 18 | package cloud_function 19 | 20 | import ( 21 | fmt "fmt" 22 | proto "github.com/golang/protobuf/proto" 23 | math "math" 24 | ) 25 | 26 | // Reference imports to suppress errors if they are not otherwise used. 27 | var _ = proto.Marshal 28 | var _ = fmt.Errorf 29 | var _ = math.Inf 30 | 31 | // This is a compile-time assertion to ensure that this generated file 32 | // is compatible with the proto package it is being compiled against. 33 | // A compilation error at this line likely means your copy of the 34 | // proto package needs to be updated. 35 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 36 | 37 | type BMP280Reading struct { 38 | Temperature *float32 `protobuf:"fixed32,1,opt,name=temperature" json:"temperature,omitempty"` 39 | Pressure *float32 `protobuf:"fixed32,2,opt,name=pressure" json:"pressure,omitempty"` 40 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 41 | XXX_unrecognized []byte `json:"-"` 42 | XXX_sizecache int32 `json:"-"` 43 | } 44 | 45 | func (m *BMP280Reading) Reset() { *m = BMP280Reading{} } 46 | func (m *BMP280Reading) String() string { return proto.CompactTextString(m) } 47 | func (*BMP280Reading) ProtoMessage() {} 48 | func (*BMP280Reading) Descriptor() ([]byte, []int) { 49 | return fileDescriptor_c749425f02243e2d, []int{0} 50 | } 51 | 52 | func (m *BMP280Reading) XXX_Unmarshal(b []byte) error { 53 | return xxx_messageInfo_BMP280Reading.Unmarshal(m, b) 54 | } 55 | func (m *BMP280Reading) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 56 | return xxx_messageInfo_BMP280Reading.Marshal(b, m, deterministic) 57 | } 58 | func (m *BMP280Reading) XXX_Merge(src proto.Message) { 59 | xxx_messageInfo_BMP280Reading.Merge(m, src) 60 | } 61 | func (m *BMP280Reading) XXX_Size() int { 62 | return xxx_messageInfo_BMP280Reading.Size(m) 63 | } 64 | func (m *BMP280Reading) XXX_DiscardUnknown() { 65 | xxx_messageInfo_BMP280Reading.DiscardUnknown(m) 66 | } 67 | 68 | var xxx_messageInfo_BMP280Reading proto.InternalMessageInfo 69 | 70 | func (m *BMP280Reading) GetTemperature() float32 { 71 | if m != nil && m.Temperature != nil { 72 | return *m.Temperature 73 | } 74 | return 0 75 | } 76 | 77 | func (m *BMP280Reading) GetPressure() float32 { 78 | if m != nil && m.Pressure != nil { 79 | return *m.Pressure 80 | } 81 | return 0 82 | } 83 | 84 | type SCD30Reading struct { 85 | Temperature *float32 `protobuf:"fixed32,1,opt,name=temperature" json:"temperature,omitempty"` 86 | Humidity *float32 `protobuf:"fixed32,2,opt,name=humidity" json:"humidity,omitempty"` 87 | Co2 *float32 `protobuf:"fixed32,3,opt,name=co2" json:"co2,omitempty"` 88 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 89 | XXX_unrecognized []byte `json:"-"` 90 | XXX_sizecache int32 `json:"-"` 91 | } 92 | 93 | func (m *SCD30Reading) Reset() { *m = SCD30Reading{} } 94 | func (m *SCD30Reading) String() string { return proto.CompactTextString(m) } 95 | func (*SCD30Reading) ProtoMessage() {} 96 | func (*SCD30Reading) Descriptor() ([]byte, []int) { 97 | return fileDescriptor_c749425f02243e2d, []int{1} 98 | } 99 | 100 | func (m *SCD30Reading) XXX_Unmarshal(b []byte) error { 101 | return xxx_messageInfo_SCD30Reading.Unmarshal(m, b) 102 | } 103 | func (m *SCD30Reading) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 104 | return xxx_messageInfo_SCD30Reading.Marshal(b, m, deterministic) 105 | } 106 | func (m *SCD30Reading) XXX_Merge(src proto.Message) { 107 | xxx_messageInfo_SCD30Reading.Merge(m, src) 108 | } 109 | func (m *SCD30Reading) XXX_Size() int { 110 | return xxx_messageInfo_SCD30Reading.Size(m) 111 | } 112 | func (m *SCD30Reading) XXX_DiscardUnknown() { 113 | xxx_messageInfo_SCD30Reading.DiscardUnknown(m) 114 | } 115 | 116 | var xxx_messageInfo_SCD30Reading proto.InternalMessageInfo 117 | 118 | func (m *SCD30Reading) GetTemperature() float32 { 119 | if m != nil && m.Temperature != nil { 120 | return *m.Temperature 121 | } 122 | return 0 123 | } 124 | 125 | func (m *SCD30Reading) GetHumidity() float32 { 126 | if m != nil && m.Humidity != nil { 127 | return *m.Humidity 128 | } 129 | return 0 130 | } 131 | 132 | func (m *SCD30Reading) GetCo2() float32 { 133 | if m != nil && m.Co2 != nil { 134 | return *m.Co2 135 | } 136 | return 0 137 | } 138 | 139 | type SGP30Reading struct { 140 | Co2 *uint32 `protobuf:"varint,1,opt,name=co2" json:"co2,omitempty"` 141 | Tvoc *uint32 `protobuf:"varint,2,opt,name=tvoc" json:"tvoc,omitempty"` 142 | BaselineCo2 *uint32 `protobuf:"varint,3,opt,name=baseline_co2,json=baselineCo2" json:"baseline_co2,omitempty"` 143 | BaselineTvoc *uint32 `protobuf:"varint,4,opt,name=baseline_tvoc,json=baselineTvoc" json:"baseline_tvoc,omitempty"` 144 | H2 *uint32 `protobuf:"varint,5,opt,name=h2" json:"h2,omitempty"` 145 | Ethanol *uint32 `protobuf:"varint,6,opt,name=ethanol" json:"ethanol,omitempty"` 146 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 147 | XXX_unrecognized []byte `json:"-"` 148 | XXX_sizecache int32 `json:"-"` 149 | } 150 | 151 | func (m *SGP30Reading) Reset() { *m = SGP30Reading{} } 152 | func (m *SGP30Reading) String() string { return proto.CompactTextString(m) } 153 | func (*SGP30Reading) ProtoMessage() {} 154 | func (*SGP30Reading) Descriptor() ([]byte, []int) { 155 | return fileDescriptor_c749425f02243e2d, []int{2} 156 | } 157 | 158 | func (m *SGP30Reading) XXX_Unmarshal(b []byte) error { 159 | return xxx_messageInfo_SGP30Reading.Unmarshal(m, b) 160 | } 161 | func (m *SGP30Reading) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 162 | return xxx_messageInfo_SGP30Reading.Marshal(b, m, deterministic) 163 | } 164 | func (m *SGP30Reading) XXX_Merge(src proto.Message) { 165 | xxx_messageInfo_SGP30Reading.Merge(m, src) 166 | } 167 | func (m *SGP30Reading) XXX_Size() int { 168 | return xxx_messageInfo_SGP30Reading.Size(m) 169 | } 170 | func (m *SGP30Reading) XXX_DiscardUnknown() { 171 | xxx_messageInfo_SGP30Reading.DiscardUnknown(m) 172 | } 173 | 174 | var xxx_messageInfo_SGP30Reading proto.InternalMessageInfo 175 | 176 | func (m *SGP30Reading) GetCo2() uint32 { 177 | if m != nil && m.Co2 != nil { 178 | return *m.Co2 179 | } 180 | return 0 181 | } 182 | 183 | func (m *SGP30Reading) GetTvoc() uint32 { 184 | if m != nil && m.Tvoc != nil { 185 | return *m.Tvoc 186 | } 187 | return 0 188 | } 189 | 190 | func (m *SGP30Reading) GetBaselineCo2() uint32 { 191 | if m != nil && m.BaselineCo2 != nil { 192 | return *m.BaselineCo2 193 | } 194 | return 0 195 | } 196 | 197 | func (m *SGP30Reading) GetBaselineTvoc() uint32 { 198 | if m != nil && m.BaselineTvoc != nil { 199 | return *m.BaselineTvoc 200 | } 201 | return 0 202 | } 203 | 204 | func (m *SGP30Reading) GetH2() uint32 { 205 | if m != nil && m.H2 != nil { 206 | return *m.H2 207 | } 208 | return 0 209 | } 210 | 211 | func (m *SGP30Reading) GetEthanol() uint32 { 212 | if m != nil && m.Ethanol != nil { 213 | return *m.Ethanol 214 | } 215 | return 0 216 | } 217 | 218 | type SMUART04LReading struct { 219 | Pm10Smoke *uint32 `protobuf:"varint,1,opt,name=pm10_smoke,json=pm10Smoke" json:"pm10_smoke,omitempty"` 220 | Pm25Smoke *uint32 `protobuf:"varint,2,opt,name=pm25_smoke,json=pm25Smoke" json:"pm25_smoke,omitempty"` 221 | Pm100Smoke *uint32 `protobuf:"varint,3,opt,name=pm100_smoke,json=pm100Smoke" json:"pm100_smoke,omitempty"` 222 | Pm10Env *uint32 `protobuf:"varint,4,opt,name=pm10_env,json=pm10Env" json:"pm10_env,omitempty"` 223 | Pm25Env *uint32 `protobuf:"varint,5,opt,name=pm25_env,json=pm25Env" json:"pm25_env,omitempty"` 224 | Pm100Env *uint32 `protobuf:"varint,6,opt,name=pm100_env,json=pm100Env" json:"pm100_env,omitempty"` 225 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 226 | XXX_unrecognized []byte `json:"-"` 227 | XXX_sizecache int32 `json:"-"` 228 | } 229 | 230 | func (m *SMUART04LReading) Reset() { *m = SMUART04LReading{} } 231 | func (m *SMUART04LReading) String() string { return proto.CompactTextString(m) } 232 | func (*SMUART04LReading) ProtoMessage() {} 233 | func (*SMUART04LReading) Descriptor() ([]byte, []int) { 234 | return fileDescriptor_c749425f02243e2d, []int{3} 235 | } 236 | 237 | func (m *SMUART04LReading) XXX_Unmarshal(b []byte) error { 238 | return xxx_messageInfo_SMUART04LReading.Unmarshal(m, b) 239 | } 240 | func (m *SMUART04LReading) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 241 | return xxx_messageInfo_SMUART04LReading.Marshal(b, m, deterministic) 242 | } 243 | func (m *SMUART04LReading) XXX_Merge(src proto.Message) { 244 | xxx_messageInfo_SMUART04LReading.Merge(m, src) 245 | } 246 | func (m *SMUART04LReading) XXX_Size() int { 247 | return xxx_messageInfo_SMUART04LReading.Size(m) 248 | } 249 | func (m *SMUART04LReading) XXX_DiscardUnknown() { 250 | xxx_messageInfo_SMUART04LReading.DiscardUnknown(m) 251 | } 252 | 253 | var xxx_messageInfo_SMUART04LReading proto.InternalMessageInfo 254 | 255 | func (m *SMUART04LReading) GetPm10Smoke() uint32 { 256 | if m != nil && m.Pm10Smoke != nil { 257 | return *m.Pm10Smoke 258 | } 259 | return 0 260 | } 261 | 262 | func (m *SMUART04LReading) GetPm25Smoke() uint32 { 263 | if m != nil && m.Pm25Smoke != nil { 264 | return *m.Pm25Smoke 265 | } 266 | return 0 267 | } 268 | 269 | func (m *SMUART04LReading) GetPm100Smoke() uint32 { 270 | if m != nil && m.Pm100Smoke != nil { 271 | return *m.Pm100Smoke 272 | } 273 | return 0 274 | } 275 | 276 | func (m *SMUART04LReading) GetPm10Env() uint32 { 277 | if m != nil && m.Pm10Env != nil { 278 | return *m.Pm10Env 279 | } 280 | return 0 281 | } 282 | 283 | func (m *SMUART04LReading) GetPm25Env() uint32 { 284 | if m != nil && m.Pm25Env != nil { 285 | return *m.Pm25Env 286 | } 287 | return 0 288 | } 289 | 290 | func (m *SMUART04LReading) GetPm100Env() uint32 { 291 | if m != nil && m.Pm100Env != nil { 292 | return *m.Pm100Env 293 | } 294 | return 0 295 | } 296 | 297 | type SensorUpdate struct { 298 | Timestamp *uint64 `protobuf:"varint,1,opt,name=timestamp" json:"timestamp,omitempty"` 299 | Bmp280 *BMP280Reading `protobuf:"bytes,2,opt,name=bmp280" json:"bmp280,omitempty"` 300 | Scd30 *SCD30Reading `protobuf:"bytes,3,opt,name=scd30" json:"scd30,omitempty"` 301 | Sgp30 *SGP30Reading `protobuf:"bytes,4,opt,name=sgp30" json:"sgp30,omitempty"` 302 | Smuart04L *SMUART04LReading `protobuf:"bytes,5,opt,name=smuart04l" json:"smuart04l,omitempty"` 303 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 304 | XXX_unrecognized []byte `json:"-"` 305 | XXX_sizecache int32 `json:"-"` 306 | } 307 | 308 | func (m *SensorUpdate) Reset() { *m = SensorUpdate{} } 309 | func (m *SensorUpdate) String() string { return proto.CompactTextString(m) } 310 | func (*SensorUpdate) ProtoMessage() {} 311 | func (*SensorUpdate) Descriptor() ([]byte, []int) { 312 | return fileDescriptor_c749425f02243e2d, []int{4} 313 | } 314 | 315 | func (m *SensorUpdate) XXX_Unmarshal(b []byte) error { 316 | return xxx_messageInfo_SensorUpdate.Unmarshal(m, b) 317 | } 318 | func (m *SensorUpdate) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 319 | return xxx_messageInfo_SensorUpdate.Marshal(b, m, deterministic) 320 | } 321 | func (m *SensorUpdate) XXX_Merge(src proto.Message) { 322 | xxx_messageInfo_SensorUpdate.Merge(m, src) 323 | } 324 | func (m *SensorUpdate) XXX_Size() int { 325 | return xxx_messageInfo_SensorUpdate.Size(m) 326 | } 327 | func (m *SensorUpdate) XXX_DiscardUnknown() { 328 | xxx_messageInfo_SensorUpdate.DiscardUnknown(m) 329 | } 330 | 331 | var xxx_messageInfo_SensorUpdate proto.InternalMessageInfo 332 | 333 | func (m *SensorUpdate) GetTimestamp() uint64 { 334 | if m != nil && m.Timestamp != nil { 335 | return *m.Timestamp 336 | } 337 | return 0 338 | } 339 | 340 | func (m *SensorUpdate) GetBmp280() *BMP280Reading { 341 | if m != nil { 342 | return m.Bmp280 343 | } 344 | return nil 345 | } 346 | 347 | func (m *SensorUpdate) GetScd30() *SCD30Reading { 348 | if m != nil { 349 | return m.Scd30 350 | } 351 | return nil 352 | } 353 | 354 | func (m *SensorUpdate) GetSgp30() *SGP30Reading { 355 | if m != nil { 356 | return m.Sgp30 357 | } 358 | return nil 359 | } 360 | 361 | func (m *SensorUpdate) GetSmuart04L() *SMUART04LReading { 362 | if m != nil { 363 | return m.Smuart04L 364 | } 365 | return nil 366 | } 367 | 368 | func init() { 369 | proto.RegisterType((*BMP280Reading)(nil), "gastranslator.BMP280Reading") 370 | proto.RegisterType((*SCD30Reading)(nil), "gastranslator.SCD30Reading") 371 | proto.RegisterType((*SGP30Reading)(nil), "gastranslator.SGP30Reading") 372 | proto.RegisterType((*SMUART04LReading)(nil), "gastranslator.SMUART04LReading") 373 | proto.RegisterType((*SensorUpdate)(nil), "gastranslator.SensorUpdate") 374 | } 375 | 376 | func init() { proto.RegisterFile("sensor.proto", fileDescriptor_c749425f02243e2d) } 377 | 378 | var fileDescriptor_c749425f02243e2d = []byte{ 379 | // 439 bytes of a gzipped FileDescriptorProto 380 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x92, 0xb1, 0x6e, 0xdb, 0x30, 381 | 0x10, 0x86, 0x61, 0xc5, 0x4e, 0xe2, 0x93, 0x54, 0x04, 0x9c, 0xd4, 0x26, 0x45, 0x52, 0x75, 0xe9, 382 | 0x64, 0x28, 0xb4, 0x0d, 0x64, 0xe9, 0xd0, 0xa6, 0x45, 0x97, 0x1a, 0x08, 0xa8, 0x64, 0x6d, 0xc0, 383 | 0xd8, 0x84, 0x2d, 0x54, 0x12, 0x09, 0x92, 0x36, 0xd0, 0x27, 0xe8, 0x9b, 0xf4, 0x49, 0xfa, 0x60, 384 | 0x05, 0x8f, 0xa2, 0xed, 0x18, 0x5d, 0xb2, 0xf1, 0xee, 0xfb, 0xff, 0xdf, 0x77, 0xd6, 0x41, 0x62, 385 | 0x44, 0x6b, 0xa4, 0x1e, 0x29, 0x2d, 0xad, 0x24, 0xe9, 0x92, 0x1b, 0xab, 0x79, 0x6b, 0x6a, 0x6e, 386 | 0xa5, 0xce, 0x67, 0x90, 0x7e, 0x9e, 0xdd, 0xd1, 0x9b, 0x82, 0x09, 0xbe, 0xa8, 0xda, 0x25, 0xb9, 387 | 0x82, 0xd8, 0x8a, 0x46, 0x09, 0xcd, 0xed, 0x5a, 0x8b, 0xac, 0x77, 0xd5, 0xfb, 0x10, 0xb1, 0xfd, 388 | 0x16, 0x79, 0x03, 0xa7, 0x4a, 0x0b, 0x63, 0x1c, 0x8e, 0x10, 0x6f, 0xeb, 0xfc, 0x07, 0x24, 0xe5, 389 | 0xed, 0x97, 0xf1, 0xcb, 0xd2, 0x56, 0xeb, 0xa6, 0x5a, 0x54, 0xf6, 0x57, 0x48, 0x0b, 0x35, 0x39, 390 | 0x83, 0xa3, 0xb9, 0xa4, 0xd9, 0x11, 0xb6, 0xdd, 0x33, 0xff, 0xd3, 0x83, 0xa4, 0xfc, 0x76, 0xb7, 391 | 0xfb, 0x81, 0x4e, 0xe2, 0x82, 0x53, 0x94, 0x10, 0x02, 0x7d, 0xbb, 0x91, 0x73, 0x0c, 0x4b, 0x19, 392 | 0xbe, 0xc9, 0x3b, 0x48, 0x9e, 0xb8, 0x11, 0x75, 0xd5, 0x8a, 0xc7, 0x90, 0x98, 0xb2, 0x38, 0xf4, 393 | 0x6e, 0x25, 0x25, 0xef, 0x21, 0xdd, 0x4a, 0xd0, 0xdf, 0x47, 0xcd, 0xd6, 0x77, 0xef, 0x72, 0x5e, 394 | 0x41, 0xb4, 0xa2, 0xd9, 0x00, 0x49, 0xb4, 0xa2, 0x24, 0x83, 0x13, 0x61, 0x57, 0xbc, 0x95, 0x75, 395 | 0x76, 0x8c, 0xcd, 0x50, 0xe6, 0x7f, 0x7b, 0x70, 0x56, 0xce, 0x1e, 0x3e, 0xb1, 0xfb, 0x62, 0xf2, 396 | 0x3d, 0x0c, 0xfb, 0x16, 0x40, 0x35, 0xd7, 0xc5, 0xa3, 0x69, 0xe4, 0x4f, 0xd1, 0xcd, 0x3c, 0x74, 397 | 0x9d, 0xd2, 0x35, 0x3c, 0xa6, 0xd3, 0x0e, 0x47, 0x01, 0xd3, 0xa9, 0xc7, 0x97, 0x10, 0x3b, 0x6d, 398 | 0xb0, 0xfb, 0x1d, 0x30, 0xb0, 0xf3, 0xbf, 0x86, 0x53, 0x8c, 0x17, 0xed, 0xa6, 0x9b, 0xfe, 0xc4, 399 | 0xd5, 0x5f, 0xdb, 0x8d, 0x47, 0x74, 0x8a, 0x68, 0x10, 0x10, 0x9d, 0x3a, 0x74, 0x0e, 0x43, 0x1f, 400 | 0xeb, 0x98, 0xdf, 0x02, 0x63, 0x9c, 0x2f, 0xff, 0x1d, 0x41, 0x52, 0xe2, 0xf9, 0x3c, 0xa8, 0x05, 401 | 0xb7, 0x82, 0x5c, 0xc0, 0xd0, 0x56, 0x8d, 0x30, 0x96, 0x37, 0x0a, 0x37, 0xe8, 0xb3, 0x5d, 0x83, 402 | 0x4c, 0xe0, 0xf8, 0xa9, 0x51, 0xf4, 0xa6, 0xc0, 0xe9, 0x63, 0x7a, 0x31, 0x7a, 0x76, 0x6d, 0xa3, 403 | 0x67, 0xa7, 0xc6, 0x3a, 0x2d, 0xb9, 0x86, 0x81, 0x99, 0x2f, 0xc6, 0x05, 0xae, 0x14, 0xd3, 0xf3, 404 | 0x03, 0xd3, 0xfe, 0x41, 0x31, 0xaf, 0x44, 0xcb, 0x52, 0x8d, 0x0b, 0xdc, 0xf3, 0x3f, 0x96, 0xbd, 405 | 0x13, 0x61, 0x5e, 0x49, 0x3e, 0xc2, 0xd0, 0x34, 0x6b, 0xae, 0x6d, 0x31, 0xa9, 0xf1, 0x3f, 0x88, 406 | 0xe9, 0xe5, 0xa1, 0xed, 0xe0, 0x83, 0xb1, 0x9d, 0xe3, 0x5f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x01, 407 | 0x3b, 0x84, 0xe6, 0x46, 0x03, 0x00, 0x00, 408 | } 409 | -------------------------------------------------------------------------------- /cloud_function/sensor.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | package gastranslator; 4 | 5 | message BMP280Reading { 6 | optional float temperature = 1; 7 | optional float pressure = 2; 8 | } 9 | 10 | message SCD30Reading { 11 | optional float temperature = 1; 12 | optional float humidity = 2; 13 | optional float co2 = 3; 14 | } 15 | 16 | message SGP30Reading { 17 | optional uint32 co2 = 1; 18 | optional uint32 tvoc = 2; 19 | optional uint32 baseline_co2 = 3; 20 | optional uint32 baseline_tvoc = 4; 21 | optional uint32 h2 = 5; 22 | optional uint32 ethanol = 6; 23 | } 24 | 25 | message SMUART04LReading { 26 | optional uint32 pm10_smoke = 1; 27 | optional uint32 pm25_smoke = 2; 28 | optional uint32 pm100_smoke = 3; 29 | optional uint32 pm10_env = 4; 30 | optional uint32 pm25_env = 5; 31 | optional uint32 pm100_env = 6; 32 | } 33 | 34 | message SensorUpdate { 35 | optional uint64 timestamp = 1; 36 | optional BMP280Reading bmp280 = 2; 37 | optional SCD30Reading scd30 = 3; 38 | optional SGP30Reading sgp30 = 4; 39 | optional SMUART04LReading smuart04l = 5; 40 | } 41 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /firmware/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # The following lines of boilerplate have to be in your project's 16 | # CMakeLists in this exact order for cmake to work correctly 17 | cmake_minimum_required(VERSION 3.5) 18 | 19 | include($ENV{IDF_PATH}/tools/cmake/project.cmake) 20 | project(hello-world) 21 | target_add_binary_data(hello-world.elf "signature_verification_key.bin" BINARY) 22 | target_add_binary_data(hello-world.elf "googleca.pem" TEXT) -------------------------------------------------------------------------------- /firmware/Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # 16 | # This is a project Makefile. It is assumed the directory this Makefile resides in is a 17 | # project subdirectory. 18 | # 19 | 20 | PROJECT_NAME := hello-world 21 | 22 | include $(IDF_PATH)/make/project.mk 23 | 24 | -------------------------------------------------------------------------------- /firmware/components/google-mqtt/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | file(GLOB_RECURSE GOOGLE_SRCS LIST_DIRECTORIES false "google-cloud-iot-arduino/src/*.cpp") 16 | file(GLOB_RECURSE MQTT_SRCS LIST_DIRECTORIES false "mqtt/src/*.cpp" "mqtt/src/*.c") 17 | set(COMPONENT_SRCS "google-mqtt.cc" "sensor.pb-c.c" ${GOOGLE_SRCS} ${MQTT_SRCS}) 18 | set(COMPONENT_ADD_INCLUDEDIRS "." "mqtt/src") 19 | set(COMPONENT_REQUIRES "arduino" "protobuf-c" "sensor-wrapper" "esp_https_ota") 20 | register_component() -------------------------------------------------------------------------------- /firmware/components/google-mqtt/Kconfig: -------------------------------------------------------------------------------- 1 | menu "Google IoT Configuration" 2 | 3 | config GIOT_PROJECT_ID 4 | string "Google Cloud Project ID" 5 | default "project_id" 6 | help 7 | Google Cloud project ID 8 | 9 | config GIOT_LOCATION 10 | string "Google Cloud location" 11 | default "us-central1-a" 12 | help 13 | Google Cloud region 14 | 15 | config GIOT_REGISTRY_ID 16 | string "Google IoT Core Registry ID" 17 | default "registry_id" 18 | help 19 | Google IoT Core registry id 20 | 21 | config GIOT_WIFI_AP 22 | string "Google IoT Core WiFi AP" 23 | default "" 24 | help 25 | Wireless AP to connect to. 26 | 27 | config GIOT_WIFI_PASSWORD 28 | string "Google IoT Core WiFi Password" 29 | default "" 30 | help 31 | Password of the wireless AP to connect to. 32 | 33 | endmenu -------------------------------------------------------------------------------- /firmware/components/google-mqtt/component.mk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/iot-environment-sensors/34ece839e73afe0aa6a91994cf8d90f4b40878bb/firmware/components/google-mqtt/component.mk -------------------------------------------------------------------------------- /firmware/components/google-mqtt/google-cloud-iot-arduino: -------------------------------------------------------------------------------- 1 | ../../../third_party/google_cloud_iot_arduino -------------------------------------------------------------------------------- /firmware/components/google-mqtt/google-mqtt.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include "google-mqtt.h" 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | #include "google-cloud-iot-arduino/src/CloudIoTCore.h" 25 | #include "google-cloud-iot-arduino/src/CloudIoTCoreMqtt.h" 26 | #include "mqtt/src/MQTT.h" 27 | #include "sensor.pb-c.h" 28 | #include "sensor_types.h" 29 | #include "sensor_wrapper.h" 30 | 31 | const char kNtpPrimary[] = "pool.ntp.org"; 32 | const char kNtpSecondary[] = "time.nist.gov"; 33 | 34 | extern const char googleca_pem_start[] asm("_binary_googleca_pem_start"); 35 | 36 | const size_t kPrivateKeySize = 95; 37 | char private_key[kPrivateKeySize + 1]; 38 | 39 | const size_t kDeviceIdSize = 12; 40 | char device_id[kDeviceIdSize + 1]; 41 | 42 | Client *client_; 43 | CloudIoTCoreDevice *device_; 44 | CloudIoTCoreMqtt *mqtt_; 45 | MQTTClient *mqtt_client_; 46 | unsigned long iss_ = 0; 47 | const int jwt_exp_secs = 3600; // Maximum 24H (3600*24) 48 | String jwt_; 49 | std::vector sensors_; 50 | std::vector temperature_sensors_; 51 | 52 | unsigned long last_update = 0; 53 | 54 | String getJwt() { 55 | iss_ = time(nullptr); 56 | Serial.println("MQTT: Refreshing JWT"); 57 | jwt_ = device_->createJWT(iss_, jwt_exp_secs); 58 | return jwt_; 59 | } 60 | 61 | bool GetFile(const char *path, size_t expected_size, char *contents) { 62 | File file = SPIFFS.open(path); 63 | if (!file || file.isDirectory()) { 64 | Serial.printf("SPIFFS: File not present: %s\n", path); 65 | return false; 66 | } 67 | 68 | if (file.readBytes(contents, expected_size) < expected_size) { 69 | Serial.printf("SPIFFS: File %s not correct length.\n", path); 70 | return false; 71 | } 72 | return true; 73 | } 74 | 75 | void messageReceived(String &topic, String &payload) { 76 | static const String update_prefix = "update: "; 77 | static const String temp_calibration_prefix = "temp_calibration: "; 78 | Serial.printf("MQTT: Received data for topic %s: %s\n", topic.c_str(), 79 | payload.c_str()); 80 | if (topic.endsWith("commands") && payload.startsWith(update_prefix)) { 81 | const char *url = 82 | payload.c_str() + update_prefix.length(); // Len of "update: " 83 | Serial.printf("MQTT: Got OTA update command using URL %s\n", url); 84 | esp_http_client_config_t config{}; 85 | config.url = url; 86 | config.cert_pem = googleca_pem_start; 87 | const esp_err_t ret = esp_https_ota(&config); 88 | if (ret == ESP_OK) { 89 | Serial.println("OTA update okay, restarting."); 90 | esp_restart(); 91 | } else { 92 | Serial.println("OTA update failed."); 93 | } 94 | } 95 | if (topic.endsWith("commands") && 96 | payload.startsWith(temp_calibration_prefix)) { 97 | const float actual_temp = 98 | payload.substring(temp_calibration_prefix.length()).toFloat(); 99 | for (TemperatureSensor *sensor : temperature_sensors_) { 100 | reinterpret_cast(sensor)->CalibrateTemperature( 101 | actual_temp); 102 | } 103 | } 104 | if (topic.endsWith("commands") && payload.startsWith("reset")) { 105 | esp_restart(); 106 | } 107 | } 108 | 109 | void WifiConnect() { 110 | Serial.printf("WiFi: Connecting to WiFi..."); 111 | WiFi.mode(WIFI_STA); 112 | WiFi.begin(CONFIG_GIOT_WIFI_AP, CONFIG_GIOT_WIFI_PASSWORD); 113 | 114 | int iterations_left = 100; 115 | while (iterations_left > 0 && WiFi.status() != WL_CONNECTED) { 116 | vTaskDelay(250 / portTICK_PERIOD_MS); 117 | Serial.print("."); 118 | iterations_left--; 119 | } 120 | Serial.printf("\n"); 121 | if (WiFi.status() != WL_CONNECTED) { 122 | Serial.println("WiFI: Connection failed, restarting."); 123 | esp_restart(); 124 | return; 125 | } 126 | Serial.printf("WiFi: Connected: %s\n", WiFi.localIP().toString().c_str()); 127 | } 128 | 129 | void WifiReconnect() { 130 | Serial.printf("WiFi: Disconnecting from WiFi."); 131 | WiFi.disconnect(); 132 | WiFi.mode(WIFI_OFF); 133 | Serial.printf("WiFi: Delaying 500ms."); 134 | vTaskDelay(500 / portTICK_PERIOD_MS); 135 | WifiConnect(); 136 | } 137 | 138 | void SetupGoogleMQTT(std::vector sensors, 139 | std::vector temperature_sensors) { 140 | // Give everything else a chance to start up properly. 141 | vTaskDelay(10000 / portTICK_PERIOD_MS); 142 | WifiConnect(); 143 | sensors_ = sensors; 144 | temperature_sensors_ = temperature_sensors; 145 | Serial.println("MQTT: Setting up Google MQTT"); 146 | configTime(0, 0, kNtpPrimary, kNtpSecondary); 147 | Serial.println("MQTT: Waiting on time sync..."); 148 | while (time(nullptr) < 1510644967) { 149 | vTaskDelay(10 / portTICK_PERIOD_MS); 150 | } 151 | Serial.printf("MQTT: Time synced @%ld\n", time(nullptr)); 152 | 153 | device_id[kDeviceIdSize] = 0; 154 | if (!GetFile("/devid.dat", kDeviceIdSize, device_id)) { 155 | Serial.println("MQTT: Couldn't get device ID."); 156 | vTaskDelete(NULL); 157 | return; 158 | } 159 | 160 | private_key[kPrivateKeySize] = 0; 161 | if (!GetFile("/ec_private.dat", kPrivateKeySize, private_key)) { 162 | Serial.println("MQTT: Couldn't get private key."); 163 | vTaskDelete(NULL); 164 | return; 165 | } 166 | 167 | device_ = 168 | new CloudIoTCoreDevice(CONFIG_GIOT_PROJECT_ID, CONFIG_GIOT_LOCATION, 169 | CONFIG_GIOT_REGISTRY_ID, device_id, private_key); 170 | client_ = new WiFiClientSecure(); 171 | mqtt_client_ = new MQTTClient(512); 172 | mqtt_client_->setOptions(180, true, 1000); 173 | mqtt_ = new CloudIoTCoreMqtt(mqtt_client_, client_, device_); 174 | mqtt_->setUseLts(true); 175 | mqtt_->startMQTT(); 176 | Serial.println("MQTT: initialized."); 177 | } 178 | 179 | void UpdateSensors() { 180 | SensorUpdate update = SENSOR_UPDATE__INIT; 181 | 182 | BMP280Reading bmp280 = BMP280_READING__INIT; 183 | update.bmp280 = &bmp280; 184 | SCD30Reading scd30 = SCD30_READING__INIT; 185 | update.scd30 = &scd30; 186 | SGP30Reading sgp30 = SGP30_READING__INIT; 187 | update.sgp30 = &sgp30; 188 | SMUART04LReading smuart04l = SMUART04_LREADING__INIT; 189 | update.smuart04l = &smuart04l; 190 | 191 | update.has_timestamp = true; 192 | update.timestamp = time(nullptr); 193 | 194 | for (SensorWrapper *sensor : sensors_) { 195 | sensor->UpdateSensor(&update); 196 | } 197 | 198 | size_t update_size = sensor_update__get_packed_size(&update); 199 | uint8_t update_packed[update_size + 1]; 200 | update_packed[update_size] = 0; 201 | sensor_update__pack(&update, update_packed); 202 | 203 | const bool result = 204 | mqtt_->publishTelemetry((char *)update_packed, update_size); 205 | if (result) { 206 | Serial.println("MQTT: Published update."); 207 | } else { 208 | Serial.println("MQTT: Update false?"); 209 | } 210 | } 211 | 212 | void LoopGoogleMQTT() { 213 | while (true) { 214 | if (WiFi.status() != WL_CONNECTED) { 215 | WifiReconnect(); 216 | } 217 | 218 | if (!mqtt_client_->connected()) { 219 | Serial.println("MQTT: Google MQTT disconnected, reconnecting"); 220 | mqtt_->mqttConnect(); 221 | continue; 222 | } 223 | mqtt_client_->loop(); 224 | 225 | Serial.println("MQTT: Google MQTT connected"); 226 | const time_t time_now = time(nullptr); 227 | if (time_now - last_update > 10) { 228 | Serial.println("MQTT: Publishing telemetry"); 229 | UpdateSensors(); 230 | last_update = time_now; 231 | } 232 | vTaskDelay(2000 / portTICK_PERIOD_MS); 233 | } 234 | } 235 | 236 | void GoogleMQTTWatchdog() { 237 | while (true) { 238 | vTaskDelay(30000 / portTICK_PERIOD_MS); 239 | const time_t time_now = time(nullptr); 240 | if (last_update == 0) { 241 | continue; 242 | } 243 | if (time_now - last_update > 120) { 244 | Serial.println("MQTT: >60s since last update, resetting."); 245 | esp_restart(); 246 | } 247 | } 248 | } -------------------------------------------------------------------------------- /firmware/components/google-mqtt/google-mqtt.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef GOOGLE_MQTT_H_ 18 | #define GOOGLE_MQTT_H_ 19 | 20 | #include 21 | 22 | #include "sensor_types.h" 23 | #include "sensor_wrapper.h" 24 | 25 | void SetupGoogleMQTT(std::vector sensors, 26 | std::vector temperature_sensors); 27 | void LoopGoogleMQTT(); 28 | void GoogleMQTTWatchdog(); 29 | 30 | #endif -------------------------------------------------------------------------------- /firmware/components/google-mqtt/mqtt: -------------------------------------------------------------------------------- 1 | ../../../third_party/arduino_mqtt -------------------------------------------------------------------------------- /firmware/components/google-mqtt/sensor.pb-c.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* Generated by the protocol buffer compiler. DO NOT EDIT! */ 18 | /* Generated from: sensor.proto */ 19 | 20 | /* Do not generate deprecated warnings for self */ 21 | #ifndef PROTOBUF_C__NO_DEPRECATED 22 | #define PROTOBUF_C__NO_DEPRECATED 23 | #endif 24 | 25 | #include "sensor.pb-c.h" 26 | void bmp280_reading__init 27 | (BMP280Reading *message) 28 | { 29 | static const BMP280Reading init_value = BMP280_READING__INIT; 30 | *message = init_value; 31 | } 32 | size_t bmp280_reading__get_packed_size 33 | (const BMP280Reading *message) 34 | { 35 | assert(message->base.descriptor == &bmp280_reading__descriptor); 36 | return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); 37 | } 38 | size_t bmp280_reading__pack 39 | (const BMP280Reading *message, 40 | uint8_t *out) 41 | { 42 | assert(message->base.descriptor == &bmp280_reading__descriptor); 43 | return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); 44 | } 45 | size_t bmp280_reading__pack_to_buffer 46 | (const BMP280Reading *message, 47 | ProtobufCBuffer *buffer) 48 | { 49 | assert(message->base.descriptor == &bmp280_reading__descriptor); 50 | return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); 51 | } 52 | BMP280Reading * 53 | bmp280_reading__unpack 54 | (ProtobufCAllocator *allocator, 55 | size_t len, 56 | const uint8_t *data) 57 | { 58 | return (BMP280Reading *) 59 | protobuf_c_message_unpack (&bmp280_reading__descriptor, 60 | allocator, len, data); 61 | } 62 | void bmp280_reading__free_unpacked 63 | (BMP280Reading *message, 64 | ProtobufCAllocator *allocator) 65 | { 66 | if(!message) 67 | return; 68 | assert(message->base.descriptor == &bmp280_reading__descriptor); 69 | protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); 70 | } 71 | void scd30_reading__init 72 | (SCD30Reading *message) 73 | { 74 | static const SCD30Reading init_value = SCD30_READING__INIT; 75 | *message = init_value; 76 | } 77 | size_t scd30_reading__get_packed_size 78 | (const SCD30Reading *message) 79 | { 80 | assert(message->base.descriptor == &scd30_reading__descriptor); 81 | return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); 82 | } 83 | size_t scd30_reading__pack 84 | (const SCD30Reading *message, 85 | uint8_t *out) 86 | { 87 | assert(message->base.descriptor == &scd30_reading__descriptor); 88 | return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); 89 | } 90 | size_t scd30_reading__pack_to_buffer 91 | (const SCD30Reading *message, 92 | ProtobufCBuffer *buffer) 93 | { 94 | assert(message->base.descriptor == &scd30_reading__descriptor); 95 | return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); 96 | } 97 | SCD30Reading * 98 | scd30_reading__unpack 99 | (ProtobufCAllocator *allocator, 100 | size_t len, 101 | const uint8_t *data) 102 | { 103 | return (SCD30Reading *) 104 | protobuf_c_message_unpack (&scd30_reading__descriptor, 105 | allocator, len, data); 106 | } 107 | void scd30_reading__free_unpacked 108 | (SCD30Reading *message, 109 | ProtobufCAllocator *allocator) 110 | { 111 | if(!message) 112 | return; 113 | assert(message->base.descriptor == &scd30_reading__descriptor); 114 | protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); 115 | } 116 | void sgp30_reading__init 117 | (SGP30Reading *message) 118 | { 119 | static const SGP30Reading init_value = SGP30_READING__INIT; 120 | *message = init_value; 121 | } 122 | size_t sgp30_reading__get_packed_size 123 | (const SGP30Reading *message) 124 | { 125 | assert(message->base.descriptor == &sgp30_reading__descriptor); 126 | return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); 127 | } 128 | size_t sgp30_reading__pack 129 | (const SGP30Reading *message, 130 | uint8_t *out) 131 | { 132 | assert(message->base.descriptor == &sgp30_reading__descriptor); 133 | return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); 134 | } 135 | size_t sgp30_reading__pack_to_buffer 136 | (const SGP30Reading *message, 137 | ProtobufCBuffer *buffer) 138 | { 139 | assert(message->base.descriptor == &sgp30_reading__descriptor); 140 | return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); 141 | } 142 | SGP30Reading * 143 | sgp30_reading__unpack 144 | (ProtobufCAllocator *allocator, 145 | size_t len, 146 | const uint8_t *data) 147 | { 148 | return (SGP30Reading *) 149 | protobuf_c_message_unpack (&sgp30_reading__descriptor, 150 | allocator, len, data); 151 | } 152 | void sgp30_reading__free_unpacked 153 | (SGP30Reading *message, 154 | ProtobufCAllocator *allocator) 155 | { 156 | if(!message) 157 | return; 158 | assert(message->base.descriptor == &sgp30_reading__descriptor); 159 | protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); 160 | } 161 | void smuart04_lreading__init 162 | (SMUART04LReading *message) 163 | { 164 | static const SMUART04LReading init_value = SMUART04_LREADING__INIT; 165 | *message = init_value; 166 | } 167 | size_t smuart04_lreading__get_packed_size 168 | (const SMUART04LReading *message) 169 | { 170 | assert(message->base.descriptor == &smuart04_lreading__descriptor); 171 | return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); 172 | } 173 | size_t smuart04_lreading__pack 174 | (const SMUART04LReading *message, 175 | uint8_t *out) 176 | { 177 | assert(message->base.descriptor == &smuart04_lreading__descriptor); 178 | return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); 179 | } 180 | size_t smuart04_lreading__pack_to_buffer 181 | (const SMUART04LReading *message, 182 | ProtobufCBuffer *buffer) 183 | { 184 | assert(message->base.descriptor == &smuart04_lreading__descriptor); 185 | return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); 186 | } 187 | SMUART04LReading * 188 | smuart04_lreading__unpack 189 | (ProtobufCAllocator *allocator, 190 | size_t len, 191 | const uint8_t *data) 192 | { 193 | return (SMUART04LReading *) 194 | protobuf_c_message_unpack (&smuart04_lreading__descriptor, 195 | allocator, len, data); 196 | } 197 | void smuart04_lreading__free_unpacked 198 | (SMUART04LReading *message, 199 | ProtobufCAllocator *allocator) 200 | { 201 | if(!message) 202 | return; 203 | assert(message->base.descriptor == &smuart04_lreading__descriptor); 204 | protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); 205 | } 206 | void sensor_update__init 207 | (SensorUpdate *message) 208 | { 209 | static const SensorUpdate init_value = SENSOR_UPDATE__INIT; 210 | *message = init_value; 211 | } 212 | size_t sensor_update__get_packed_size 213 | (const SensorUpdate *message) 214 | { 215 | assert(message->base.descriptor == &sensor_update__descriptor); 216 | return protobuf_c_message_get_packed_size ((const ProtobufCMessage*)(message)); 217 | } 218 | size_t sensor_update__pack 219 | (const SensorUpdate *message, 220 | uint8_t *out) 221 | { 222 | assert(message->base.descriptor == &sensor_update__descriptor); 223 | return protobuf_c_message_pack ((const ProtobufCMessage*)message, out); 224 | } 225 | size_t sensor_update__pack_to_buffer 226 | (const SensorUpdate *message, 227 | ProtobufCBuffer *buffer) 228 | { 229 | assert(message->base.descriptor == &sensor_update__descriptor); 230 | return protobuf_c_message_pack_to_buffer ((const ProtobufCMessage*)message, buffer); 231 | } 232 | SensorUpdate * 233 | sensor_update__unpack 234 | (ProtobufCAllocator *allocator, 235 | size_t len, 236 | const uint8_t *data) 237 | { 238 | return (SensorUpdate *) 239 | protobuf_c_message_unpack (&sensor_update__descriptor, 240 | allocator, len, data); 241 | } 242 | void sensor_update__free_unpacked 243 | (SensorUpdate *message, 244 | ProtobufCAllocator *allocator) 245 | { 246 | if(!message) 247 | return; 248 | assert(message->base.descriptor == &sensor_update__descriptor); 249 | protobuf_c_message_free_unpacked ((ProtobufCMessage*)message, allocator); 250 | } 251 | static const ProtobufCFieldDescriptor bmp280_reading__field_descriptors[2] = 252 | { 253 | { 254 | "temperature", 255 | 1, 256 | PROTOBUF_C_LABEL_OPTIONAL, 257 | PROTOBUF_C_TYPE_FLOAT, 258 | offsetof(BMP280Reading, has_temperature), 259 | offsetof(BMP280Reading, temperature), 260 | NULL, 261 | NULL, 262 | 0, /* flags */ 263 | 0,NULL,NULL /* reserved1,reserved2, etc */ 264 | }, 265 | { 266 | "pressure", 267 | 2, 268 | PROTOBUF_C_LABEL_OPTIONAL, 269 | PROTOBUF_C_TYPE_FLOAT, 270 | offsetof(BMP280Reading, has_pressure), 271 | offsetof(BMP280Reading, pressure), 272 | NULL, 273 | NULL, 274 | 0, /* flags */ 275 | 0,NULL,NULL /* reserved1,reserved2, etc */ 276 | }, 277 | }; 278 | static const unsigned bmp280_reading__field_indices_by_name[] = { 279 | 1, /* field[1] = pressure */ 280 | 0, /* field[0] = temperature */ 281 | }; 282 | static const ProtobufCIntRange bmp280_reading__number_ranges[1 + 1] = 283 | { 284 | { 1, 0 }, 285 | { 0, 2 } 286 | }; 287 | const ProtobufCMessageDescriptor bmp280_reading__descriptor = 288 | { 289 | PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, 290 | "BMP280Reading", 291 | "BMP280Reading", 292 | "BMP280Reading", 293 | "", 294 | sizeof(BMP280Reading), 295 | 2, 296 | bmp280_reading__field_descriptors, 297 | bmp280_reading__field_indices_by_name, 298 | 1, bmp280_reading__number_ranges, 299 | (ProtobufCMessageInit) bmp280_reading__init, 300 | NULL,NULL,NULL /* reserved[123] */ 301 | }; 302 | static const ProtobufCFieldDescriptor scd30_reading__field_descriptors[3] = 303 | { 304 | { 305 | "temperature", 306 | 1, 307 | PROTOBUF_C_LABEL_OPTIONAL, 308 | PROTOBUF_C_TYPE_FLOAT, 309 | offsetof(SCD30Reading, has_temperature), 310 | offsetof(SCD30Reading, temperature), 311 | NULL, 312 | NULL, 313 | 0, /* flags */ 314 | 0,NULL,NULL /* reserved1,reserved2, etc */ 315 | }, 316 | { 317 | "humidity", 318 | 2, 319 | PROTOBUF_C_LABEL_OPTIONAL, 320 | PROTOBUF_C_TYPE_FLOAT, 321 | offsetof(SCD30Reading, has_humidity), 322 | offsetof(SCD30Reading, humidity), 323 | NULL, 324 | NULL, 325 | 0, /* flags */ 326 | 0,NULL,NULL /* reserved1,reserved2, etc */ 327 | }, 328 | { 329 | "co2", 330 | 3, 331 | PROTOBUF_C_LABEL_OPTIONAL, 332 | PROTOBUF_C_TYPE_FLOAT, 333 | offsetof(SCD30Reading, has_co2), 334 | offsetof(SCD30Reading, co2), 335 | NULL, 336 | NULL, 337 | 0, /* flags */ 338 | 0,NULL,NULL /* reserved1,reserved2, etc */ 339 | }, 340 | }; 341 | static const unsigned scd30_reading__field_indices_by_name[] = { 342 | 2, /* field[2] = co2 */ 343 | 1, /* field[1] = humidity */ 344 | 0, /* field[0] = temperature */ 345 | }; 346 | static const ProtobufCIntRange scd30_reading__number_ranges[1 + 1] = 347 | { 348 | { 1, 0 }, 349 | { 0, 3 } 350 | }; 351 | const ProtobufCMessageDescriptor scd30_reading__descriptor = 352 | { 353 | PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, 354 | "SCD30Reading", 355 | "SCD30Reading", 356 | "SCD30Reading", 357 | "", 358 | sizeof(SCD30Reading), 359 | 3, 360 | scd30_reading__field_descriptors, 361 | scd30_reading__field_indices_by_name, 362 | 1, scd30_reading__number_ranges, 363 | (ProtobufCMessageInit) scd30_reading__init, 364 | NULL,NULL,NULL /* reserved[123] */ 365 | }; 366 | static const ProtobufCFieldDescriptor sgp30_reading__field_descriptors[6] = 367 | { 368 | { 369 | "co2", 370 | 1, 371 | PROTOBUF_C_LABEL_OPTIONAL, 372 | PROTOBUF_C_TYPE_UINT32, 373 | offsetof(SGP30Reading, has_co2), 374 | offsetof(SGP30Reading, co2), 375 | NULL, 376 | NULL, 377 | 0, /* flags */ 378 | 0,NULL,NULL /* reserved1,reserved2, etc */ 379 | }, 380 | { 381 | "tvoc", 382 | 2, 383 | PROTOBUF_C_LABEL_OPTIONAL, 384 | PROTOBUF_C_TYPE_UINT32, 385 | offsetof(SGP30Reading, has_tvoc), 386 | offsetof(SGP30Reading, tvoc), 387 | NULL, 388 | NULL, 389 | 0, /* flags */ 390 | 0,NULL,NULL /* reserved1,reserved2, etc */ 391 | }, 392 | { 393 | "baseline_co2", 394 | 3, 395 | PROTOBUF_C_LABEL_OPTIONAL, 396 | PROTOBUF_C_TYPE_UINT32, 397 | offsetof(SGP30Reading, has_baseline_co2), 398 | offsetof(SGP30Reading, baseline_co2), 399 | NULL, 400 | NULL, 401 | 0, /* flags */ 402 | 0,NULL,NULL /* reserved1,reserved2, etc */ 403 | }, 404 | { 405 | "baseline_tvoc", 406 | 4, 407 | PROTOBUF_C_LABEL_OPTIONAL, 408 | PROTOBUF_C_TYPE_UINT32, 409 | offsetof(SGP30Reading, has_baseline_tvoc), 410 | offsetof(SGP30Reading, baseline_tvoc), 411 | NULL, 412 | NULL, 413 | 0, /* flags */ 414 | 0,NULL,NULL /* reserved1,reserved2, etc */ 415 | }, 416 | { 417 | "h2", 418 | 5, 419 | PROTOBUF_C_LABEL_OPTIONAL, 420 | PROTOBUF_C_TYPE_UINT32, 421 | offsetof(SGP30Reading, has_h2), 422 | offsetof(SGP30Reading, h2), 423 | NULL, 424 | NULL, 425 | 0, /* flags */ 426 | 0,NULL,NULL /* reserved1,reserved2, etc */ 427 | }, 428 | { 429 | "ethanol", 430 | 6, 431 | PROTOBUF_C_LABEL_OPTIONAL, 432 | PROTOBUF_C_TYPE_UINT32, 433 | offsetof(SGP30Reading, has_ethanol), 434 | offsetof(SGP30Reading, ethanol), 435 | NULL, 436 | NULL, 437 | 0, /* flags */ 438 | 0,NULL,NULL /* reserved1,reserved2, etc */ 439 | }, 440 | }; 441 | static const unsigned sgp30_reading__field_indices_by_name[] = { 442 | 2, /* field[2] = baseline_co2 */ 443 | 3, /* field[3] = baseline_tvoc */ 444 | 0, /* field[0] = co2 */ 445 | 5, /* field[5] = ethanol */ 446 | 4, /* field[4] = h2 */ 447 | 1, /* field[1] = tvoc */ 448 | }; 449 | static const ProtobufCIntRange sgp30_reading__number_ranges[1 + 1] = 450 | { 451 | { 1, 0 }, 452 | { 0, 6 } 453 | }; 454 | const ProtobufCMessageDescriptor sgp30_reading__descriptor = 455 | { 456 | PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, 457 | "SGP30Reading", 458 | "SGP30Reading", 459 | "SGP30Reading", 460 | "", 461 | sizeof(SGP30Reading), 462 | 6, 463 | sgp30_reading__field_descriptors, 464 | sgp30_reading__field_indices_by_name, 465 | 1, sgp30_reading__number_ranges, 466 | (ProtobufCMessageInit) sgp30_reading__init, 467 | NULL,NULL,NULL /* reserved[123] */ 468 | }; 469 | static const ProtobufCFieldDescriptor smuart04_lreading__field_descriptors[6] = 470 | { 471 | { 472 | "pm10_smoke", 473 | 1, 474 | PROTOBUF_C_LABEL_OPTIONAL, 475 | PROTOBUF_C_TYPE_UINT32, 476 | offsetof(SMUART04LReading, has_pm10_smoke), 477 | offsetof(SMUART04LReading, pm10_smoke), 478 | NULL, 479 | NULL, 480 | 0, /* flags */ 481 | 0,NULL,NULL /* reserved1,reserved2, etc */ 482 | }, 483 | { 484 | "pm25_smoke", 485 | 2, 486 | PROTOBUF_C_LABEL_OPTIONAL, 487 | PROTOBUF_C_TYPE_UINT32, 488 | offsetof(SMUART04LReading, has_pm25_smoke), 489 | offsetof(SMUART04LReading, pm25_smoke), 490 | NULL, 491 | NULL, 492 | 0, /* flags */ 493 | 0,NULL,NULL /* reserved1,reserved2, etc */ 494 | }, 495 | { 496 | "pm100_smoke", 497 | 3, 498 | PROTOBUF_C_LABEL_OPTIONAL, 499 | PROTOBUF_C_TYPE_UINT32, 500 | offsetof(SMUART04LReading, has_pm100_smoke), 501 | offsetof(SMUART04LReading, pm100_smoke), 502 | NULL, 503 | NULL, 504 | 0, /* flags */ 505 | 0,NULL,NULL /* reserved1,reserved2, etc */ 506 | }, 507 | { 508 | "pm10_env", 509 | 4, 510 | PROTOBUF_C_LABEL_OPTIONAL, 511 | PROTOBUF_C_TYPE_UINT32, 512 | offsetof(SMUART04LReading, has_pm10_env), 513 | offsetof(SMUART04LReading, pm10_env), 514 | NULL, 515 | NULL, 516 | 0, /* flags */ 517 | 0,NULL,NULL /* reserved1,reserved2, etc */ 518 | }, 519 | { 520 | "pm25_env", 521 | 5, 522 | PROTOBUF_C_LABEL_OPTIONAL, 523 | PROTOBUF_C_TYPE_UINT32, 524 | offsetof(SMUART04LReading, has_pm25_env), 525 | offsetof(SMUART04LReading, pm25_env), 526 | NULL, 527 | NULL, 528 | 0, /* flags */ 529 | 0,NULL,NULL /* reserved1,reserved2, etc */ 530 | }, 531 | { 532 | "pm100_env", 533 | 6, 534 | PROTOBUF_C_LABEL_OPTIONAL, 535 | PROTOBUF_C_TYPE_UINT32, 536 | offsetof(SMUART04LReading, has_pm100_env), 537 | offsetof(SMUART04LReading, pm100_env), 538 | NULL, 539 | NULL, 540 | 0, /* flags */ 541 | 0,NULL,NULL /* reserved1,reserved2, etc */ 542 | }, 543 | }; 544 | static const unsigned smuart04_lreading__field_indices_by_name[] = { 545 | 5, /* field[5] = pm100_env */ 546 | 2, /* field[2] = pm100_smoke */ 547 | 3, /* field[3] = pm10_env */ 548 | 0, /* field[0] = pm10_smoke */ 549 | 4, /* field[4] = pm25_env */ 550 | 1, /* field[1] = pm25_smoke */ 551 | }; 552 | static const ProtobufCIntRange smuart04_lreading__number_ranges[1 + 1] = 553 | { 554 | { 1, 0 }, 555 | { 0, 6 } 556 | }; 557 | const ProtobufCMessageDescriptor smuart04_lreading__descriptor = 558 | { 559 | PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, 560 | "SMUART04LReading", 561 | "SMUART04LReading", 562 | "SMUART04LReading", 563 | "", 564 | sizeof(SMUART04LReading), 565 | 6, 566 | smuart04_lreading__field_descriptors, 567 | smuart04_lreading__field_indices_by_name, 568 | 1, smuart04_lreading__number_ranges, 569 | (ProtobufCMessageInit) smuart04_lreading__init, 570 | NULL,NULL,NULL /* reserved[123] */ 571 | }; 572 | static const ProtobufCFieldDescriptor sensor_update__field_descriptors[5] = 573 | { 574 | { 575 | "timestamp", 576 | 1, 577 | PROTOBUF_C_LABEL_OPTIONAL, 578 | PROTOBUF_C_TYPE_UINT64, 579 | offsetof(SensorUpdate, has_timestamp), 580 | offsetof(SensorUpdate, timestamp), 581 | NULL, 582 | NULL, 583 | 0, /* flags */ 584 | 0,NULL,NULL /* reserved1,reserved2, etc */ 585 | }, 586 | { 587 | "bmp280", 588 | 2, 589 | PROTOBUF_C_LABEL_OPTIONAL, 590 | PROTOBUF_C_TYPE_MESSAGE, 591 | 0, /* quantifier_offset */ 592 | offsetof(SensorUpdate, bmp280), 593 | &bmp280_reading__descriptor, 594 | NULL, 595 | 0, /* flags */ 596 | 0,NULL,NULL /* reserved1,reserved2, etc */ 597 | }, 598 | { 599 | "scd30", 600 | 3, 601 | PROTOBUF_C_LABEL_OPTIONAL, 602 | PROTOBUF_C_TYPE_MESSAGE, 603 | 0, /* quantifier_offset */ 604 | offsetof(SensorUpdate, scd30), 605 | &scd30_reading__descriptor, 606 | NULL, 607 | 0, /* flags */ 608 | 0,NULL,NULL /* reserved1,reserved2, etc */ 609 | }, 610 | { 611 | "sgp30", 612 | 4, 613 | PROTOBUF_C_LABEL_OPTIONAL, 614 | PROTOBUF_C_TYPE_MESSAGE, 615 | 0, /* quantifier_offset */ 616 | offsetof(SensorUpdate, sgp30), 617 | &sgp30_reading__descriptor, 618 | NULL, 619 | 0, /* flags */ 620 | 0,NULL,NULL /* reserved1,reserved2, etc */ 621 | }, 622 | { 623 | "smuart04l", 624 | 5, 625 | PROTOBUF_C_LABEL_OPTIONAL, 626 | PROTOBUF_C_TYPE_MESSAGE, 627 | 0, /* quantifier_offset */ 628 | offsetof(SensorUpdate, smuart04l), 629 | &smuart04_lreading__descriptor, 630 | NULL, 631 | 0, /* flags */ 632 | 0,NULL,NULL /* reserved1,reserved2, etc */ 633 | }, 634 | }; 635 | static const unsigned sensor_update__field_indices_by_name[] = { 636 | 1, /* field[1] = bmp280 */ 637 | 2, /* field[2] = scd30 */ 638 | 3, /* field[3] = sgp30 */ 639 | 4, /* field[4] = smuart04l */ 640 | 0, /* field[0] = timestamp */ 641 | }; 642 | static const ProtobufCIntRange sensor_update__number_ranges[1 + 1] = 643 | { 644 | { 1, 0 }, 645 | { 0, 5 } 646 | }; 647 | const ProtobufCMessageDescriptor sensor_update__descriptor = 648 | { 649 | PROTOBUF_C__MESSAGE_DESCRIPTOR_MAGIC, 650 | "SensorUpdate", 651 | "SensorUpdate", 652 | "SensorUpdate", 653 | "", 654 | sizeof(SensorUpdate), 655 | 5, 656 | sensor_update__field_descriptors, 657 | sensor_update__field_indices_by_name, 658 | 1, sensor_update__number_ranges, 659 | (ProtobufCMessageInit) sensor_update__init, 660 | NULL,NULL,NULL /* reserved[123] */ 661 | }; 662 | -------------------------------------------------------------------------------- /firmware/components/google-mqtt/sensor.pb-c.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* Generated by the protocol buffer compiler. DO NOT EDIT! */ 18 | /* Generated from: sensor.proto */ 19 | 20 | #ifndef PROTOBUF_C_sensor_2eproto__INCLUDED 21 | #define PROTOBUF_C_sensor_2eproto__INCLUDED 22 | 23 | #include 24 | 25 | PROTOBUF_C__BEGIN_DECLS 26 | 27 | #if PROTOBUF_C_VERSION_NUMBER < 1000000 28 | # error This file was generated by a newer version of protoc-c which is incompatible with your libprotobuf-c headers. Please update your headers. 29 | #elif 1003001 < PROTOBUF_C_MIN_COMPILER_VERSION 30 | # error This file was generated by an older version of protoc-c which is incompatible with your libprotobuf-c headers. Please regenerate this file with a newer version of protoc-c. 31 | #endif 32 | 33 | 34 | typedef struct _BMP280Reading BMP280Reading; 35 | typedef struct _SCD30Reading SCD30Reading; 36 | typedef struct _SGP30Reading SGP30Reading; 37 | typedef struct _SMUART04LReading SMUART04LReading; 38 | typedef struct _SensorUpdate SensorUpdate; 39 | 40 | 41 | /* --- enums --- */ 42 | 43 | 44 | /* --- messages --- */ 45 | 46 | struct _BMP280Reading 47 | { 48 | ProtobufCMessage base; 49 | protobuf_c_boolean has_temperature; 50 | float temperature; 51 | protobuf_c_boolean has_pressure; 52 | float pressure; 53 | }; 54 | #define BMP280_READING__INIT \ 55 | { PROTOBUF_C_MESSAGE_INIT (&bmp280_reading__descriptor) \ 56 | , 0, 0, 0, 0 } 57 | 58 | 59 | struct _SCD30Reading 60 | { 61 | ProtobufCMessage base; 62 | protobuf_c_boolean has_temperature; 63 | float temperature; 64 | protobuf_c_boolean has_humidity; 65 | float humidity; 66 | protobuf_c_boolean has_co2; 67 | float co2; 68 | }; 69 | #define SCD30_READING__INIT \ 70 | { PROTOBUF_C_MESSAGE_INIT (&scd30_reading__descriptor) \ 71 | , 0, 0, 0, 0, 0, 0 } 72 | 73 | 74 | struct _SGP30Reading 75 | { 76 | ProtobufCMessage base; 77 | protobuf_c_boolean has_co2; 78 | uint32_t co2; 79 | protobuf_c_boolean has_tvoc; 80 | uint32_t tvoc; 81 | protobuf_c_boolean has_baseline_co2; 82 | uint32_t baseline_co2; 83 | protobuf_c_boolean has_baseline_tvoc; 84 | uint32_t baseline_tvoc; 85 | protobuf_c_boolean has_h2; 86 | uint32_t h2; 87 | protobuf_c_boolean has_ethanol; 88 | uint32_t ethanol; 89 | }; 90 | #define SGP30_READING__INIT \ 91 | { PROTOBUF_C_MESSAGE_INIT (&sgp30_reading__descriptor) \ 92 | , 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } 93 | 94 | 95 | struct _SMUART04LReading 96 | { 97 | ProtobufCMessage base; 98 | protobuf_c_boolean has_pm10_smoke; 99 | uint32_t pm10_smoke; 100 | protobuf_c_boolean has_pm25_smoke; 101 | uint32_t pm25_smoke; 102 | protobuf_c_boolean has_pm100_smoke; 103 | uint32_t pm100_smoke; 104 | protobuf_c_boolean has_pm10_env; 105 | uint32_t pm10_env; 106 | protobuf_c_boolean has_pm25_env; 107 | uint32_t pm25_env; 108 | protobuf_c_boolean has_pm100_env; 109 | uint32_t pm100_env; 110 | }; 111 | #define SMUART04_LREADING__INIT \ 112 | { PROTOBUF_C_MESSAGE_INIT (&smuart04_lreading__descriptor) \ 113 | , 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } 114 | 115 | 116 | struct _SensorUpdate 117 | { 118 | ProtobufCMessage base; 119 | protobuf_c_boolean has_timestamp; 120 | uint64_t timestamp; 121 | BMP280Reading *bmp280; 122 | SCD30Reading *scd30; 123 | SGP30Reading *sgp30; 124 | SMUART04LReading *smuart04l; 125 | }; 126 | #define SENSOR_UPDATE__INIT \ 127 | { PROTOBUF_C_MESSAGE_INIT (&sensor_update__descriptor) \ 128 | , 0, 0, NULL, NULL, NULL, NULL } 129 | 130 | 131 | /* BMP280Reading methods */ 132 | void bmp280_reading__init 133 | (BMP280Reading *message); 134 | size_t bmp280_reading__get_packed_size 135 | (const BMP280Reading *message); 136 | size_t bmp280_reading__pack 137 | (const BMP280Reading *message, 138 | uint8_t *out); 139 | size_t bmp280_reading__pack_to_buffer 140 | (const BMP280Reading *message, 141 | ProtobufCBuffer *buffer); 142 | BMP280Reading * 143 | bmp280_reading__unpack 144 | (ProtobufCAllocator *allocator, 145 | size_t len, 146 | const uint8_t *data); 147 | void bmp280_reading__free_unpacked 148 | (BMP280Reading *message, 149 | ProtobufCAllocator *allocator); 150 | /* SCD30Reading methods */ 151 | void scd30_reading__init 152 | (SCD30Reading *message); 153 | size_t scd30_reading__get_packed_size 154 | (const SCD30Reading *message); 155 | size_t scd30_reading__pack 156 | (const SCD30Reading *message, 157 | uint8_t *out); 158 | size_t scd30_reading__pack_to_buffer 159 | (const SCD30Reading *message, 160 | ProtobufCBuffer *buffer); 161 | SCD30Reading * 162 | scd30_reading__unpack 163 | (ProtobufCAllocator *allocator, 164 | size_t len, 165 | const uint8_t *data); 166 | void scd30_reading__free_unpacked 167 | (SCD30Reading *message, 168 | ProtobufCAllocator *allocator); 169 | /* SGP30Reading methods */ 170 | void sgp30_reading__init 171 | (SGP30Reading *message); 172 | size_t sgp30_reading__get_packed_size 173 | (const SGP30Reading *message); 174 | size_t sgp30_reading__pack 175 | (const SGP30Reading *message, 176 | uint8_t *out); 177 | size_t sgp30_reading__pack_to_buffer 178 | (const SGP30Reading *message, 179 | ProtobufCBuffer *buffer); 180 | SGP30Reading * 181 | sgp30_reading__unpack 182 | (ProtobufCAllocator *allocator, 183 | size_t len, 184 | const uint8_t *data); 185 | void sgp30_reading__free_unpacked 186 | (SGP30Reading *message, 187 | ProtobufCAllocator *allocator); 188 | /* SMUART04LReading methods */ 189 | void smuart04_lreading__init 190 | (SMUART04LReading *message); 191 | size_t smuart04_lreading__get_packed_size 192 | (const SMUART04LReading *message); 193 | size_t smuart04_lreading__pack 194 | (const SMUART04LReading *message, 195 | uint8_t *out); 196 | size_t smuart04_lreading__pack_to_buffer 197 | (const SMUART04LReading *message, 198 | ProtobufCBuffer *buffer); 199 | SMUART04LReading * 200 | smuart04_lreading__unpack 201 | (ProtobufCAllocator *allocator, 202 | size_t len, 203 | const uint8_t *data); 204 | void smuart04_lreading__free_unpacked 205 | (SMUART04LReading *message, 206 | ProtobufCAllocator *allocator); 207 | /* SensorUpdate methods */ 208 | void sensor_update__init 209 | (SensorUpdate *message); 210 | size_t sensor_update__get_packed_size 211 | (const SensorUpdate *message); 212 | size_t sensor_update__pack 213 | (const SensorUpdate *message, 214 | uint8_t *out); 215 | size_t sensor_update__pack_to_buffer 216 | (const SensorUpdate *message, 217 | ProtobufCBuffer *buffer); 218 | SensorUpdate * 219 | sensor_update__unpack 220 | (ProtobufCAllocator *allocator, 221 | size_t len, 222 | const uint8_t *data); 223 | void sensor_update__free_unpacked 224 | (SensorUpdate *message, 225 | ProtobufCAllocator *allocator); 226 | /* --- per-message closures --- */ 227 | 228 | typedef void (*BMP280Reading_Closure) 229 | (const BMP280Reading *message, 230 | void *closure_data); 231 | typedef void (*SCD30Reading_Closure) 232 | (const SCD30Reading *message, 233 | void *closure_data); 234 | typedef void (*SGP30Reading_Closure) 235 | (const SGP30Reading *message, 236 | void *closure_data); 237 | typedef void (*SMUART04LReading_Closure) 238 | (const SMUART04LReading *message, 239 | void *closure_data); 240 | typedef void (*SensorUpdate_Closure) 241 | (const SensorUpdate *message, 242 | void *closure_data); 243 | 244 | /* --- services --- */ 245 | 246 | 247 | /* --- descriptors --- */ 248 | 249 | extern const ProtobufCMessageDescriptor bmp280_reading__descriptor; 250 | extern const ProtobufCMessageDescriptor scd30_reading__descriptor; 251 | extern const ProtobufCMessageDescriptor sgp30_reading__descriptor; 252 | extern const ProtobufCMessageDescriptor smuart04_lreading__descriptor; 253 | extern const ProtobufCMessageDescriptor sensor_update__descriptor; 254 | 255 | PROTOBUF_C__END_DECLS 256 | 257 | 258 | #endif /* PROTOBUF_C_sensor_2eproto__INCLUDED */ 259 | -------------------------------------------------------------------------------- /firmware/components/google-mqtt/sensor.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | message BMP280Reading { 4 | optional float temperature = 1; 5 | optional float pressure = 2; 6 | } 7 | 8 | message SCD30Reading { 9 | optional float temperature = 1; 10 | optional float humidity = 2; 11 | optional float co2 = 3; 12 | } 13 | 14 | message SGP30Reading { 15 | optional uint32 co2 = 1; 16 | optional uint32 tvoc = 2; 17 | optional uint32 baseline_co2 = 3; 18 | optional uint32 baseline_tvoc = 4; 19 | optional uint32 h2 = 5; 20 | optional uint32 ethanol = 6; 21 | } 22 | 23 | message SMUART04LReading { 24 | optional uint32 pm10_smoke = 1; 25 | optional uint32 pm25_smoke = 2; 26 | optional uint32 pm100_smoke = 3; 27 | optional uint32 pm10_env = 4; 28 | optional uint32 pm25_env = 5; 29 | optional uint32 pm100_env = 6; 30 | } 31 | 32 | message SensorUpdate { 33 | optional uint64 timestamp = 1; 34 | optional BMP280Reading bmp280 = 2; 35 | optional SCD30Reading scd30 = 3; 36 | optional SGP30Reading sgp30 = 4; 37 | optional SMUART04LReading smuart04l = 5; 38 | } 39 | -------------------------------------------------------------------------------- /firmware/components/sensor-bmp280/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set(COMPONENT_SRCS "sensor_bmp280.cc" "adafruit/Adafruit_BMP280.cpp") 16 | set(COMPONENT_ADD_INCLUDEDIRS ".") 17 | set(COMPONENT_REQUIRES "arduino" "sensor-wrapper") 18 | register_component() 19 | -------------------------------------------------------------------------------- /firmware/components/sensor-bmp280/adafruit: -------------------------------------------------------------------------------- 1 | ../../../third_party/adafruit_bmp280 -------------------------------------------------------------------------------- /firmware/components/sensor-bmp280/sensor_bmp280.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include "sensor_bmp280.h" 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include "adafruit/Adafruit_BMP280.h" 23 | #include "sensor_types.h" 24 | #include "sensor_wrapper.h" 25 | 26 | bool GetBMPFile(const char* path, size_t expected_size, char* contents) { 27 | File file = SPIFFS.open(path); 28 | if (!file || file.isDirectory()) { 29 | Serial.printf("SPIFFS: File not present: %s\n", path); 30 | return false; 31 | } 32 | 33 | if (file.readBytes(contents, expected_size) < expected_size) { 34 | Serial.printf("SPIFFS: File %s not correct length.\n", path); 35 | return false; 36 | } 37 | return true; 38 | } 39 | 40 | Bmp280Sensor::Bmp280Sensor(HardwareSerial* debug_serial, 41 | SemaphoreHandle_t* i2c_mutex) 42 | : reading_{}, debug_serial_(*debug_serial), i2c_mutex_(*i2c_mutex), temperature_offset_(0) { 43 | reading_mutex_ = xSemaphoreCreateMutex(); 44 | } 45 | 46 | void Bmp280Sensor::Init() { 47 | if (xSemaphoreTake(i2c_mutex_, 2000 / portTICK_PERIOD_MS) != pdPASS) { 48 | debug_serial_.println("BMP280: Couldn't acquire mutex for bmp280 init."); 49 | return; 50 | } 51 | if (!bmp280_.begin(BMP280_ADDRESS_ALT)) { 52 | debug_serial_.println("BMP280: Failed to initialize bmp280"); 53 | } 54 | 55 | bmp280_.setSampling(Adafruit_BMP280::MODE_NORMAL, /* Operating Mode. */ 56 | Adafruit_BMP280::SAMPLING_X16, /* Temp. oversampling */ 57 | Adafruit_BMP280::SAMPLING_X16, /* Pressure oversampling */ 58 | Adafruit_BMP280::FILTER_X16, /* Filtering. */ 59 | Adafruit_BMP280::STANDBY_MS_500); /* Standby time. */ 60 | 61 | xSemaphoreGive(i2c_mutex_); 62 | 63 | float new_offset; 64 | if(GetBMPFile("/bmp280_offset.dat", 4, (char*)&new_offset)) { 65 | debug_serial_.printf("BMP280: Read temperature offset %f\n", new_offset); 66 | temperature_offset_ = new_offset; 67 | } 68 | 69 | debug_serial_.println("BMP280: BMP280 init complete."); 70 | } 71 | 72 | void Bmp280Sensor::Loop() { 73 | while (1) { 74 | vTaskDelay(2000 / portTICK_PERIOD_MS); 75 | if (xSemaphoreTake(i2c_mutex_, 2000 / portTICK_PERIOD_MS) != pdPASS) { 76 | debug_serial_.println( 77 | "BMP280: Couldn't acquire mutex for BMP280 update."); 78 | continue; 79 | } 80 | float pressure = bmp280_.readPressure(); 81 | float temperature = bmp280_.readTemperature(); 82 | xSemaphoreGive(i2c_mutex_); 83 | 84 | debug_serial_.printf("BMP280: data %f, %f, %f\n", pressure, temperature, temperature + temperature_offset_); 85 | if (xSemaphoreTake(reading_mutex_, 2000 / portTICK_PERIOD_MS) != pdPASS) { 86 | debug_serial_.println( 87 | "BMP280: Couldn't acquire environment mutex for BMP280 update."); 88 | continue; 89 | } 90 | reading_.pressure = pressure; 91 | reading_.temperature = temperature; 92 | xSemaphoreGive(reading_mutex_); 93 | } 94 | } 95 | 96 | void Bmp280Sensor::CalibrateTemperature(float actual_temp) { 97 | if (xSemaphoreTake(reading_mutex_, 2000 / portTICK_PERIOD_MS) != pdPASS) { 98 | debug_serial_.println( 99 | "BMP280: Couldn't acquire environment mutex for BMP280 temp calibration."); 100 | return; 101 | } 102 | const float last_reading = reading_.temperature; 103 | const float new_offset = actual_temp - last_reading; 104 | debug_serial_.printf("BMP280: Changing temperature offset. Old: %f, new: %f\n", temperature_offset_, new_offset); 105 | temperature_offset_ = new_offset; 106 | xSemaphoreGive(reading_mutex_); 107 | 108 | File file = SPIFFS.open("/bmp280_offset.dat", "w"); 109 | if (!file || file.isDirectory()) { 110 | debug_serial_.println("Couldn't open offset file."); 111 | } else { 112 | if (file.write((uint8_t*)&temperature_offset_, 4) != 4) { 113 | debug_serial_.println("BMP280: Failed to write temperature offset."); 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /firmware/components/sensor-bmp280/sensor_bmp280.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef SENSOR_BMP280_H_ 18 | #define SENSOR_BMP280_H_ 19 | #include 20 | #include 21 | #include 22 | 23 | #include "adafruit/Adafruit_BMP280.h" 24 | #include "sensor.pb-c.h" 25 | #include "sensor_types.h" 26 | #include "sensor_wrapper.h" 27 | 28 | struct Bmp280Reading { 29 | float temperature; 30 | float pressure; 31 | }; 32 | 33 | class Bmp280Sensor : public SensorWrapper, 34 | public PressureSensor, 35 | public TemperatureSensor { 36 | public: 37 | Bmp280Sensor(HardwareSerial* debug_serial, SemaphoreHandle_t* i2c_mutex); 38 | void Init() override; 39 | void Loop() override; 40 | virtual std::string GetSensorName() override { return "bmp280"; } 41 | 42 | float GetPressure() override { 43 | xSemaphoreTake(reading_mutex_, portMAX_DELAY); 44 | const float pressure = reading_.pressure / 100.0f; 45 | xSemaphoreGive(reading_mutex_); 46 | return pressure; 47 | } 48 | 49 | float GetTemperature() override { 50 | xSemaphoreTake(reading_mutex_, portMAX_DELAY); 51 | const float temperature = reading_.temperature + temperature_offset_; 52 | xSemaphoreGive(reading_mutex_); 53 | return temperature; 54 | } 55 | 56 | void CalibrateTemperature(float actual_temp) override; 57 | 58 | void UpdateSensor(SensorUpdate* update) override { 59 | Serial.println("Updating BMP280 proto."); 60 | xSemaphoreTake(reading_mutex_, portMAX_DELAY); 61 | update->bmp280->temperature = reading_.temperature + temperature_offset_; 62 | update->bmp280->has_temperature = true; 63 | update->bmp280->pressure = reading_.pressure; 64 | update->bmp280->has_pressure = true; 65 | xSemaphoreGive(reading_mutex_); 66 | }; 67 | 68 | private: 69 | Bmp280Reading reading_; 70 | HardwareSerial& debug_serial_; 71 | SemaphoreHandle_t reading_mutex_; 72 | SemaphoreHandle_t& i2c_mutex_; 73 | float temperature_offset_; 74 | Adafruit_BMP280 bmp280_; 75 | }; 76 | 77 | #endif -------------------------------------------------------------------------------- /firmware/components/sensor-scd30/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set(COMPONENT_SRCS "sensor_scd30.cc" "sparkfun/src/SparkFun_SCD30_Arduino_Library.cpp") 16 | set(COMPONENT_ADD_INCLUDEDIRS ".") 17 | set(COMPONENT_REQUIRES "arduino" "sensor-wrapper") 18 | register_component() 19 | -------------------------------------------------------------------------------- /firmware/components/sensor-scd30/sensor_scd30.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include "sensor_scd30.h" 16 | 17 | #include 18 | #include 19 | 20 | Scd30Sensor::Scd30Sensor(HardwareSerial* debug_serial, 21 | SemaphoreHandle_t* i2c_mutex, 22 | PressureSensor* pressure_sensor) 23 | : reading_{}, 24 | failed_readings_(0), 25 | debug_serial_(*debug_serial), 26 | i2c_mutex_(*i2c_mutex), 27 | pressure_sensor_(pressure_sensor) { 28 | reading_mutex_ = xSemaphoreCreateMutex(); 29 | } 30 | 31 | void Scd30Sensor::Init() { 32 | if (xSemaphoreTake(i2c_mutex_, 2000 / portTICK_PERIOD_MS) != pdPASS) { 33 | debug_serial_.println("SCD30: Couldn't acquire mutex for SCD30 init."); 34 | return; 35 | } 36 | if (!scd30_.begin()) { 37 | debug_serial_.println("SCD30: Failed to initialize SCD30."); 38 | xSemaphoreGive(i2c_mutex_); 39 | return; 40 | } 41 | if (!scd30_.setAutoSelfCalibration(true)) { 42 | debug_serial_.println("SCD30: Failed to enable SCD30 self calibration."); 43 | xSemaphoreGive(i2c_mutex_); 44 | return; 45 | } 46 | xSemaphoreGive(i2c_mutex_); 47 | debug_serial_.println("SCD30: SCD30 init complete."); 48 | } 49 | 50 | void Scd30Sensor::Loop() { 51 | vTaskDelay(10000 / 52 | portTICK_PERIOD_MS); // Give it 10s for the BME680 to stabilize. 53 | int remaining_pressure_iterations = 0; 54 | uint16_t last_pressure = 0; 55 | while (1) { 56 | // Serial.println("Starting SCD30 iteration."); 57 | assert(heap_caps_check_integrity_all(true)); 58 | if (xSemaphoreTake(i2c_mutex_, 2000 / portTICK_PERIOD_MS) != pdPASS) { 59 | debug_serial_.println("SCD30: Couldn't acquire mutex for SCD30 update."); 60 | continue; 61 | } 62 | if (scd30_.dataAvailable()) { 63 | if (!scd30_.readMeasurement()) { 64 | debug_serial_.println("SCD30: Failed to read SCD30 data."); 65 | failed_readings_++; 66 | xSemaphoreGive(i2c_mutex_); 67 | continue; 68 | } 69 | if (xSemaphoreTake(reading_mutex_, 2000 / portTICK_PERIOD_MS) != pdPASS) { 70 | debug_serial_.println( 71 | "SCD30: Couldn't acquire environment mutex for SCD30 update."); 72 | xSemaphoreGive(i2c_mutex_); 73 | continue; 74 | } 75 | reading_.co2 = scd30_.getCO2(); 76 | reading_.temperature = scd30_.getTemperature(); 77 | reading_.humidity = scd30_.getHumidity(); 78 | debug_serial_.printf("SCD30: data %f, %f, %f\n", reading_.co2, 79 | reading_.humidity, reading_.temperature); 80 | xSemaphoreGive(reading_mutex_); 81 | } 82 | if (remaining_pressure_iterations == 0) { 83 | if (pressure_sensor_ != nullptr) { 84 | const float raw_pressure = pressure_sensor_->GetPressure(); 85 | if (raw_pressure < 800.0 || raw_pressure > 1200.0) { 86 | debug_serial_.printf("SCD30: Pressure too high to update: %f\n", 87 | raw_pressure); 88 | } else { 89 | const uint16_t new_pressure = 90 | static_cast(raw_pressure + 0.5f); 91 | if (new_pressure == last_pressure) { 92 | debug_serial_.println("SCD30: Skipping pressure update."); 93 | } else { 94 | debug_serial_.printf("SCD30: Setting pressure to %d\n", 95 | new_pressure); 96 | scd30_.setAmbientPressure(new_pressure); 97 | last_pressure = new_pressure; 98 | } 99 | } 100 | } 101 | remaining_pressure_iterations = 60; // 10 minutes. 102 | } else { 103 | remaining_pressure_iterations--; 104 | } 105 | xSemaphoreGive(i2c_mutex_); 106 | assert(heap_caps_check_integrity_all(true)); 107 | const uint32_t t_ms = 2000; 108 | const TickType_t t_ticks = t_ms / portTICK_PERIOD_MS; 109 | // debug_serial_.printf("SCD30 sleeping for %ums, %u ticks\n", t_ms, 110 | // t_ticks); 111 | vTaskDelay(t_ticks); 112 | } 113 | } 114 | 115 | void Scd30Sensor::CalibrateTemperature(float actual_temp) { 116 | debug_serial_.printf("SCD30: Calibrating temperature - %f\n", actual_temp); 117 | if (xSemaphoreTake(reading_mutex_, 2000 / portTICK_PERIOD_MS) != pdPASS) { 118 | debug_serial_.println( 119 | "SCD30: Couldn't acquire environment mutex for calibration."); 120 | return; 121 | } 122 | const float original_reading = reading_.temperature; 123 | xSemaphoreGive(reading_mutex_); 124 | const float delta = actual_temp - original_reading; 125 | debug_serial_.printf("SCD30: Temperature delta %f - %f = %f\n", actual_temp, 126 | original_reading, delta); 127 | if (xSemaphoreTake(i2c_mutex_, 2000 / portTICK_PERIOD_MS) != pdPASS) { 128 | debug_serial_.println("SCD30: Couldn't acquire mutex for calibration."); 129 | return; 130 | } 131 | scd30_.sendCommand(COMMAND_SET_TEMPERATURE_OFFSET); 132 | uint8_t res[3]; 133 | Wire.requestFrom(SCD30_ADDRESS, 3); 134 | const size_t bytes_read = Wire.readBytes(res, 3); 135 | if (bytes_read != 3) { 136 | debug_serial_.printf( 137 | "SCD30: Couldn't get temperature offset, read failed (%d != 3)\n", 138 | bytes_read); 139 | xSemaphoreGive(i2c_mutex_); 140 | return; 141 | } 142 | 143 | const uint8_t crc = scd30_.computeCRC8(res, 2); 144 | if (crc != res[2]) { 145 | debug_serial_.println("SCD30: temperature offset CRC failed."); 146 | xSemaphoreGive(i2c_mutex_); 147 | return; 148 | } 149 | 150 | const uint8_t original_offset_msb = res[0]; 151 | const uint8_t original_offset_lsb = res[1]; 152 | const uint16_t original_offset = 153 | ((uint16_t)original_offset_msb << 8 | original_offset_lsb); 154 | const int32_t new_offset = original_offset + (delta * -100.0f); 155 | const uint16_t final_new_offset = 156 | (new_offset < 0 || new_offset > 2000) ? 0 : new_offset; 157 | 158 | debug_serial_.printf( 159 | "SDC30: Original offset %d, new offset %d, final offset %d\n", 160 | original_offset, new_offset, final_new_offset); 161 | scd30_.sendCommand(COMMAND_SET_TEMPERATURE_OFFSET, final_new_offset); 162 | xSemaphoreGive(i2c_mutex_); 163 | } -------------------------------------------------------------------------------- /firmware/components/sensor-scd30/sensor_scd30.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef SENSOR_SCD30_H_ 18 | #define SENSOR_SCD30_H_ 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | #include "sensor.pb-c.h" 25 | #include "sensor_types.h" 26 | #include "sensor_wrapper.h" 27 | #include "sparkfun/src/SparkFun_SCD30_Arduino_Library.h" 28 | 29 | struct Scd30Reading { 30 | float temperature; 31 | float humidity; 32 | float co2; 33 | }; 34 | 35 | class Scd30Sensor : public SensorWrapper, 36 | public TemperatureSensor, 37 | public HumiditySensor { 38 | public: 39 | Scd30Sensor(HardwareSerial* debug_serial, SemaphoreHandle_t* i2c_mutex, 40 | PressureSensor* pressure_sensor = nullptr); 41 | void Init() override; 42 | void Loop() override; 43 | virtual std::string GetSensorName() override { return "scd30"; } 44 | 45 | float GetTemperature() override { 46 | xSemaphoreTake(reading_mutex_, portMAX_DELAY); 47 | const float temperature = reading_.temperature; 48 | xSemaphoreGive(reading_mutex_); 49 | return temperature; 50 | } 51 | 52 | float GetHumidity() override { 53 | xSemaphoreTake(reading_mutex_, portMAX_DELAY); 54 | const float humidity = reading_.humidity; 55 | xSemaphoreGive(reading_mutex_); 56 | return humidity; 57 | } 58 | 59 | void CalibrateTemperature(float actual_temp) override; 60 | 61 | void UpdateSensor(SensorUpdate* update) override { 62 | xSemaphoreTake(reading_mutex_, portMAX_DELAY); 63 | update->scd30->temperature = reading_.temperature; 64 | update->scd30->has_temperature = true; 65 | update->scd30->humidity = reading_.humidity; 66 | update->scd30->has_humidity = true; 67 | update->scd30->co2 = reading_.co2; 68 | update->scd30->has_co2 = true; 69 | xSemaphoreGive(reading_mutex_); 70 | }; 71 | 72 | private: 73 | Scd30Reading reading_; 74 | uint64_t failed_readings_; 75 | HardwareSerial& debug_serial_; 76 | SemaphoreHandle_t reading_mutex_; 77 | SemaphoreHandle_t& i2c_mutex_; 78 | PressureSensor* pressure_sensor_; 79 | SCD30 scd30_; 80 | }; 81 | 82 | #endif -------------------------------------------------------------------------------- /firmware/components/sensor-scd30/sparkfun: -------------------------------------------------------------------------------- 1 | ../../../third_party/sparkfun_scd30 -------------------------------------------------------------------------------- /firmware/components/sensor-sgp30/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set(COMPONENT_SRCS "sparkfun/src/SparkFun_SGP30_Arduino_Library.cpp" "sensor_sgp30.cc") 16 | set(COMPONENT_ADD_INCLUDEDIRS ".") 17 | set(COMPONENT_REQUIRES "arduino" "sensor-wrapper") 18 | register_component() -------------------------------------------------------------------------------- /firmware/components/sensor-sgp30/sensor_sgp30.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include "sensor_sgp30.h" 16 | 17 | #include 18 | 19 | #include 20 | #include 21 | 22 | Sgp30Sensor::Sgp30Sensor(HardwareSerial* debug_serial, 23 | SemaphoreHandle_t* i2c_mutex, 24 | TemperatureSensor* temperature_sensor, 25 | HumiditySensor* humidity_sensor) 26 | : reading_{}, 27 | failed_readings_(0), 28 | debug_serial_(*debug_serial), 29 | i2c_mutex_(*i2c_mutex), 30 | reading_mutex_(xSemaphoreCreateMutex()), 31 | temperature_sensor_(temperature_sensor), 32 | humidity_sensor_(humidity_sensor), 33 | abs_humidity_hardware_format_(0), 34 | abs_humidity_(0) {} 35 | 36 | const char* baseline_filename = "/sgp30_baselines.dat"; 37 | 38 | bool GetSgpFile(const char* path, size_t expected_size, char* contents) { 39 | File file = SPIFFS.open(path); 40 | if (!file || file.isDirectory()) { 41 | Serial.printf("SPIFFS: File not present: %s\n", path); 42 | return false; 43 | } 44 | 45 | if (file.readBytes(contents, expected_size) < expected_size) { 46 | Serial.printf("SPIFFS: File %s not correct length.\n", path); 47 | return false; 48 | } 49 | return true; 50 | } 51 | 52 | void Sgp30Sensor::Init() { 53 | uint16_t baselines[2]; 54 | const bool baselines_read = 55 | GetSgpFile(baseline_filename, 4, (char*)baselines); 56 | 57 | if (xSemaphoreTake(i2c_mutex_, 2000 / portTICK_PERIOD_MS) != pdPASS) { 58 | debug_serial_.println("SGP30: Couldn't acquire mutex for SGP30 init."); 59 | return; 60 | } 61 | if (!sgp30_.begin()) { 62 | debug_serial_.println("SGP30: Failed to init SGP30."); 63 | } 64 | sgp30_.initAirQuality(); 65 | if (baselines_read) { 66 | debug_serial_.println("SGP30: Setting baselines."); 67 | sgp30_.setBaseline(baselines[0], baselines[1]); 68 | } 69 | xSemaphoreGive(i2c_mutex_); 70 | debug_serial_.println("SGP30: SGP30 init complete."); 71 | } 72 | 73 | uint16_t Sgp30Sensor::AbsHumidityForSensor(float relative_humidity, 74 | float temperature) { 75 | // From 76 | // https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/ 77 | abs_humidity_ = ((6.112 * exp((17.67 * temperature) / (temperature + 243.5)) * 78 | relative_humidity * 2.1674) / 79 | (273.15 + temperature)); 80 | debug_serial_.printf( 81 | "SGP30: Temp: %.2f°C, Hum: %.2f%%, Abs. Hum: %.2fg/m^3\n", temperature, 82 | relative_humidity, abs_humidity_); 83 | const uint32_t abs_humidity = abs_humidity_ * 1000.0f; 84 | const uint16_t ah_scaled = (uint16_t)((abs_humidity * 16777) >> 16); 85 | return ah_scaled; 86 | } 87 | 88 | void Sgp30Sensor::Loop() { 89 | int iterations_until_humidity_update = 20; 90 | int iterations_until_baseline_update = 30; 91 | while (1) { 92 | vTaskDelay(1000 / portTICK_PERIOD_MS); 93 | 94 | if (iterations_until_humidity_update == 0 && 95 | temperature_sensor_ != nullptr && humidity_sensor_ != nullptr) { 96 | float relative_humidity = humidity_sensor_->GetHumidity(); 97 | float temperature = temperature_sensor_->GetTemperature(); 98 | if (relative_humidity != 0 && temperature != 0) { 99 | abs_humidity_hardware_format_ = 100 | AbsHumidityForSensor(relative_humidity, temperature); 101 | } 102 | iterations_until_humidity_update = 60; 103 | } else { 104 | iterations_until_humidity_update--; 105 | } 106 | 107 | if (xSemaphoreTake(i2c_mutex_, 2000 / portTICK_PERIOD_MS) != pdPASS) { 108 | debug_serial_.println("SGP30: Couldn't acquire mutex for SGP30 update."); 109 | continue; 110 | } 111 | 112 | if (abs_humidity_hardware_format_ != 0) { 113 | sgp30_.setHumidity(abs_humidity_hardware_format_); 114 | } 115 | 116 | SGP30ERR result = sgp30_.measureAirQuality(); 117 | 118 | if (result != SGP30ERR::SUCCESS) { 119 | debug_serial_.printf("SGP30: SGP30 status was not success, was %u\n", 120 | result); 121 | failed_readings_++; 122 | xSemaphoreGive(i2c_mutex_); 123 | continue; 124 | } 125 | 126 | result = sgp30_.getBaseline(); 127 | if (result != SGP30ERR::SUCCESS) { 128 | debug_serial_.printf( 129 | "SGP30: SGP30 baseline status was not success, was %u\n", result); 130 | failed_readings_++; 131 | } 132 | 133 | result = sgp30_.measureRawSignals(); 134 | if (result != SGP30ERR::SUCCESS) { 135 | debug_serial_.printf( 136 | "SGP30: SGP30 raw signal status was not success, was %u\n", result); 137 | failed_readings_++; 138 | } 139 | xSemaphoreGive(i2c_mutex_); 140 | 141 | if (xSemaphoreTake(reading_mutex_, 2000 / portTICK_PERIOD_MS) != pdPASS) { 142 | debug_serial_.println( 143 | "SGP30: Couldn't acquire environment mutex for SGP30 update."); 144 | } 145 | reading_.co2 = sgp30_.CO2; 146 | reading_.tvoc = sgp30_.TVOC; 147 | reading_.baseline_co2 = sgp30_.baselineCO2; 148 | reading_.baseline_tvoc = sgp30_.baselineTVOC; 149 | reading_.h2 = sgp30_.H2; 150 | reading_.ethanol = sgp30_.ethanol; 151 | xSemaphoreGive(reading_mutex_); 152 | 153 | debug_serial_.printf("SGP30: Got readings from SGP30: "); 154 | for (int i = 0; i < 6; i++) { 155 | debug_serial_.printf("%u ", *(&reading_.co2 + i)); 156 | } 157 | debug_serial_.printf("\n"); 158 | 159 | if (iterations_until_baseline_update == 0) { 160 | debug_serial_.println("SGP30: Saving baselines."); 161 | 162 | File file = SPIFFS.open(baseline_filename, "w"); 163 | if (!file || file.isDirectory()) { 164 | debug_serial_.printf("SGP30: File not present: %s\n", 165 | baseline_filename); 166 | } else { 167 | const uint16_t baselines[] = {sgp30_.baselineCO2, sgp30_.baselineTVOC}; 168 | if (file.write((uint8_t*)baselines, 4) != 4) { 169 | debug_serial_.println("SGP30: Failed to write baselines."); 170 | } 171 | } 172 | iterations_until_baseline_update = 3600 * 24; 173 | } else { 174 | iterations_until_baseline_update--; 175 | } 176 | } 177 | } 178 | 179 | void Sgp30Sensor::UpdateSensor(SensorUpdate* update) { 180 | xSemaphoreTake(reading_mutex_, portMAX_DELAY); 181 | update->sgp30->co2 = reading_.co2; 182 | update->sgp30->has_co2 = true; 183 | update->sgp30->tvoc = reading_.tvoc; 184 | update->sgp30->has_tvoc = true; 185 | update->sgp30->baseline_co2 = reading_.baseline_co2; 186 | update->sgp30->has_baseline_co2 = true; 187 | update->sgp30->baseline_tvoc = reading_.baseline_tvoc; 188 | update->sgp30->has_baseline_tvoc = true; 189 | update->sgp30->h2 = reading_.h2; 190 | update->sgp30->has_h2 = true; 191 | update->sgp30->ethanol = reading_.ethanol; 192 | update->sgp30->has_ethanol = true; 193 | xSemaphoreGive(reading_mutex_); 194 | }; -------------------------------------------------------------------------------- /firmware/components/sensor-sgp30/sensor_sgp30.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef SENSOR_SGP30_H_ 18 | #define SENSOR_SGP30_H_ 19 | #include 20 | #include 21 | #include 22 | 23 | #include "sensor.pb-c.h" 24 | #include "sensor_types.h" 25 | #include "sensor_wrapper.h" 26 | #include "sparkfun/src/SparkFun_SGP30_Arduino_Library.h" 27 | 28 | struct Sgp30Reading { 29 | uint16_t co2; 30 | uint16_t tvoc; 31 | uint16_t baseline_co2; 32 | uint16_t baseline_tvoc; 33 | uint16_t h2; 34 | uint16_t ethanol; 35 | }; 36 | 37 | class Sgp30Sensor : public SensorWrapper { 38 | public: 39 | Sgp30Sensor(HardwareSerial* debug_serial, SemaphoreHandle_t* i2c_mutex, 40 | TemperatureSensor* temperature_sensor = nullptr, 41 | HumiditySensor* humidity_sensor = nullptr); 42 | void Init() override; 43 | void Loop() override; 44 | virtual std::string GetSensorName() override { return "sgp30"; } 45 | void UpdateSensor(SensorUpdate* update) override; 46 | 47 | private: 48 | uint16_t AbsHumidityForSensor(float relative_humidity, float temperature); 49 | Sgp30Reading reading_; 50 | uint64_t failed_readings_; 51 | SGP30 sgp30_; 52 | HardwareSerial& debug_serial_; 53 | SemaphoreHandle_t& i2c_mutex_; 54 | SemaphoreHandle_t reading_mutex_; 55 | TemperatureSensor* temperature_sensor_; 56 | HumiditySensor* humidity_sensor_; 57 | uint16_t abs_humidity_hardware_format_; 58 | float abs_humidity_; 59 | }; 60 | 61 | #endif -------------------------------------------------------------------------------- /firmware/components/sensor-sgp30/sparkfun: -------------------------------------------------------------------------------- 1 | ../../../third_party/sparkfun_sgp30 -------------------------------------------------------------------------------- /firmware/components/sensor-smuart04l/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set(COMPONENT_SRCS "sensor_smuart04l.cc") 16 | set(COMPONENT_ADD_INCLUDEDIRS ".") 17 | set(COMPONENT_REQUIRES "arduino" "sensor-wrapper") 18 | register_component() 19 | -------------------------------------------------------------------------------- /firmware/components/sensor-smuart04l/sensor_smuart04l.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include "sensor_smuart04l.h" 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include 22 | 23 | Smuart04lSensor::Smuart04lSensor(HardwareSerial* debug_serial, 24 | HardwareSerial* uart) 25 | : reading_{}, 26 | checksum_mismatches_(0), 27 | status_(0), 28 | debug_serial_(*debug_serial), 29 | uart_(*uart) { 30 | reading_mutex_ = xSemaphoreCreateMutex(); 31 | } 32 | 33 | void Smuart04lSensor::Init() { 34 | uart_.begin(9600); 35 | debug_serial_.println("SM-UART-04L init complete."); 36 | } 37 | 38 | void Smuart04lSensor::Loop() { 39 | while (1) { 40 | static uint8_t buf[32]; 41 | while (uart_.available() > 32) { 42 | uart_.readBytes(buf, 32); 43 | uint16_t calculated_checksum = 0; 44 | for (int i = 0; i < 32; i++) { 45 | if (i < 30) { 46 | calculated_checksum += buf[i]; 47 | } 48 | } 49 | uint16_t read_checksum = *(uint16_t*)(buf + 30); 50 | read_checksum = (read_checksum << 8) | (read_checksum >> 8); 51 | 52 | if (calculated_checksum != read_checksum) { 53 | Serial.printf("SM-UART-04L: Checksum mismatch, skipping."); 54 | Serial.printf(" expected checksum: %u, actual checksum: %u\n", 55 | read_checksum, calculated_checksum); 56 | checksum_mismatches_++; 57 | continue; 58 | } 59 | memcpy(&reading_, buf + 4, 12); 60 | for (int i = 0; i < 6; i++) { 61 | uint16_t* ptr = &reading_.pm10_smoke + i; 62 | *ptr = (*ptr >> 8) | (*ptr << 8); 63 | } 64 | status_ = buf[30]; 65 | Serial.printf("SM-UART-04L: %u, %u, %u, %u, %u, %u\n", 66 | reading_.pm10_smoke, reading_.pm25_smoke, 67 | reading_.pm100_smoke, reading_.pm10_env, reading_.pm25_env, 68 | reading_.pm100_env); 69 | } 70 | vTaskDelay(2000 / portTICK_PERIOD_MS); 71 | } 72 | } 73 | 74 | void Smuart04lSensor::UpdateSensor(SensorUpdate* update) { 75 | xSemaphoreTake(reading_mutex_, portMAX_DELAY); 76 | update->smuart04l->pm10_smoke = reading_.pm10_smoke; 77 | update->smuart04l->has_pm10_smoke = true; 78 | update->smuart04l->pm25_smoke = reading_.pm25_smoke; 79 | update->smuart04l->has_pm25_smoke = true; 80 | update->smuart04l->pm100_smoke = reading_.pm100_smoke; 81 | update->smuart04l->has_pm100_smoke = true; 82 | update->smuart04l->pm10_env = reading_.pm10_env; 83 | update->smuart04l->has_pm10_env = true; 84 | update->smuart04l->pm25_env = reading_.pm25_env; 85 | update->smuart04l->has_pm25_env = true; 86 | update->smuart04l->pm100_env = reading_.pm100_env; 87 | update->smuart04l->has_pm100_env = true; 88 | xSemaphoreGive(reading_mutex_); 89 | }; 90 | 91 | std::string Smuart04lSensor::GetSensorName() { return "smuart04l"; } 92 | -------------------------------------------------------------------------------- /firmware/components/sensor-smuart04l/sensor_smuart04l.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef SENSOR_UMUART04L_H_ 18 | #define SENSOR_UMUART04L_H_ 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include "sensor.pb-c.h" 26 | #include "sensor_wrapper.h" 27 | 28 | struct Smuart04lReading { 29 | uint16_t pm10_smoke; 30 | uint16_t pm25_smoke; 31 | uint16_t pm100_smoke; 32 | uint16_t pm10_env; 33 | uint16_t pm25_env; 34 | uint16_t pm100_env; 35 | }; 36 | 37 | class Smuart04lSensor : public SensorWrapper { 38 | public: 39 | Smuart04lSensor(HardwareSerial* debug_serial, HardwareSerial* uart); 40 | void Init() override; 41 | void Loop() override; 42 | std::string GetSensorName() override; 43 | void UpdateSensor(SensorUpdate* update) override; 44 | 45 | private: 46 | Smuart04lReading reading_; 47 | uint64_t checksum_mismatches_; 48 | uint8_t status_; 49 | SemaphoreHandle_t reading_mutex_; 50 | HardwareSerial& debug_serial_; 51 | HardwareSerial& uart_; 52 | }; 53 | 54 | #endif -------------------------------------------------------------------------------- /firmware/components/sensor-wrapper/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set(COMPONENT_SRCS "sensor_wrapper.cc") 16 | set(COMPONENT_ADD_INCLUDEDIRS ".") 17 | set(COMPONENT_REQUIRES "google-mqtt") 18 | register_component() 19 | -------------------------------------------------------------------------------- /firmware/components/sensor-wrapper/sensor_types.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef SENSOR_TYPES_H_ 18 | #define SENSOR_TYPES_H_ 19 | 20 | class TemperatureSensor { 21 | public: 22 | virtual float GetTemperature() = 0; 23 | virtual void CalibrateTemperature(float actual_temp) = 0; 24 | }; 25 | 26 | class HumiditySensor { 27 | public: 28 | virtual float GetHumidity() = 0; 29 | }; 30 | 31 | class PressureSensor { 32 | public: 33 | virtual float GetPressure() = 0; 34 | }; 35 | 36 | #endif -------------------------------------------------------------------------------- /firmware/components/sensor-wrapper/sensor_wrapper.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /firmware/components/sensor-wrapper/sensor_wrapper.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef SENSOR_WRAPPER_H_ 18 | #define SENSOR_WRAPPER_H_ 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include "sensor.pb-c.h" 27 | 28 | struct SensorMetric { 29 | std::string metric_name; 30 | std::string value; 31 | std::vector attributes; 32 | }; 33 | 34 | using SensorMetricList = std::vector; 35 | 36 | class SensorWrapper { 37 | public: 38 | virtual void Init() = 0; 39 | virtual void Loop() = 0; 40 | virtual std::string GetSensorName() = 0; 41 | virtual void UpdateSensor(SensorUpdate* update) { return; }; 42 | }; 43 | 44 | #endif -------------------------------------------------------------------------------- /firmware/googleca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIESjCCAzKgAwIBAgINAeO0mqGNiqmBJWlQuDANBgkqhkiG9w0BAQsFADBMMSAw 3 | HgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFs 4 | U2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjAeFw0xNzA2MTUwMDAwNDJaFw0yMTEy 5 | MTUwMDAwNDJaMEIxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVHb29nbGUgVHJ1c3Qg 6 | U2VydmljZXMxEzARBgNVBAMTCkdUUyBDQSAxTzEwggEiMA0GCSqGSIb3DQEBAQUA 7 | A4IBDwAwggEKAoIBAQDQGM9F1IvN05zkQO9+tN1pIRvJzzyOTHW5DzEZhD2ePCnv 8 | UA0Qk28FgICfKqC9EksC4T2fWBYk/jCfC3R3VZMdS/dN4ZKCEPZRrAzDsiKUDzRr 9 | mBBJ5wudgzndIMYcLe/RGGFl5yODIKgjEv/SJH/UL+dEaltN11BmsK+eQmMF++Ac 10 | xGNhr59qM/9il71I2dN8FGfcddwuaej4bXhp0LcQBbjxMcI7JP0aM3T4I+DsaxmK 11 | FsbjzaTNC9uzpFlgOIg7rR25xoynUxv8vNmkq7zdPGHXkxWY7oG9j+JkRyBABk7X 12 | rJfoucBZEqFJJSPk7XA0LKW0Y3z5oz2D0c1tJKwHAgMBAAGjggEzMIIBLzAOBgNV 13 | HQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBIGA1Ud 14 | EwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFJjR+G4Q68+b7GCfGJAboOt9Cf0rMB8G 15 | A1UdIwQYMBaAFJviB1dnHB7AagbeWbSaLd/cGYYuMDUGCCsGAQUFBwEBBCkwJzAl 16 | BggrBgEFBQcwAYYZaHR0cDovL29jc3AucGtpLmdvb2cvZ3NyMjAyBgNVHR8EKzAp 17 | MCegJaAjhiFodHRwOi8vY3JsLnBraS5nb29nL2dzcjIvZ3NyMi5jcmwwPwYDVR0g 18 | BDgwNjA0BgZngQwBAgIwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly9wa2kuZ29vZy9y 19 | ZXBvc2l0b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAGoA+Nnn78y6pRjd9XlQWNa7H 20 | TgiZ/r3RNGkmUmYHPQq6Scti9PEajvwRT2iWTHQr02fesqOqBY2ETUwgZQ+lltoN 21 | FvhsO9tvBCOIazpswWC9aJ9xju4tWDQH8NVU6YZZ/XteDSGU9YzJqPjY8q3MDxrz 22 | mqepBCf5o8mw/wJ4a2G6xzUr6Fb6T8McDO22PLRL6u3M4Tzs3A2M1j6bykJYi8wW 23 | IRdAvKLWZu/axBVbzYmqmwkm5zLSDW5nIAJbELCQCZwMH56t2Dvqofxs6BBcCFIZ 24 | USpxu6x6td0V7SvJCCosirSmIatj/9dSSVDQibet8q/7UK4v4ZUN80atnZz1yg== 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /firmware/main/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | set(COMPONENT_SRCS "main.cc") 16 | set(COMPONENT_ADD_INCLUDEDIRS "") 17 | set(COMPONENT_REQUIRES "arduino" "sensor-wrapper" "sensor-bmp280" "sensor-scd30" "sensor-smuart04l" "sensor-sgp30" "google-mqtt") 18 | register_component() 19 | -------------------------------------------------------------------------------- /firmware/main/component.mk: -------------------------------------------------------------------------------- 1 | # 2 | # "main" pseudo-component makefile. 3 | # 4 | # (Uses default behaviour of compiling all source files in directory, adding 'include' to include path.) 5 | 6 | -------------------------------------------------------------------------------- /firmware/main/main.cc: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include 27 | #include 28 | #include 29 | 30 | #include "google-mqtt.h" 31 | #include "sensor_bmp280.h" 32 | #include "sensor_scd30.h" 33 | #include "sensor_sgp30.h" 34 | #include "sensor_smuart04l.h" 35 | 36 | SemaphoreHandle_t i2c_mutex; 37 | 38 | #define WIFI_REPORTING_ENABLED 39 | static const int num_sensors = 4; 40 | HardwareSerial smuart04l_serial(1); 41 | 42 | std::array sensors; 43 | std::array, num_sensors + 2> stacks; 44 | std::array tasks; 45 | std::vector temperature_sensors; 46 | 47 | void ServerUpdateLoopMqtt(void *pv_parameters) { 48 | std::vector sensor_vector; 49 | for (SensorWrapper *sensor : sensors) { 50 | sensor_vector.push_back(sensor); 51 | } 52 | SetupGoogleMQTT(sensor_vector, temperature_sensors); 53 | LoopGoogleMQTT(); 54 | } 55 | 56 | void ServerWatchdogLoopMqtt(void *pv_parameters) { GoogleMQTTWatchdog(); } 57 | 58 | void ScanI2c() { 59 | byte error, address; 60 | int num_devices; 61 | 62 | Serial.println("Scanning..."); 63 | 64 | num_devices = 0; 65 | for (address = 1; address < 127; address++) { 66 | // The i2c_scanner uses the return value of 67 | // the Write.endTransmisstion to see if 68 | // a device did acknowledge to the address. 69 | Wire.beginTransmission(address); 70 | error = Wire.endTransmission(); 71 | 72 | if (error == 0) { 73 | Serial.print("I2C device found at address 0x"); 74 | if (address < 16) Serial.print("0"); 75 | Serial.print(address, HEX); 76 | Serial.println(" !"); 77 | 78 | num_devices++; 79 | } else if (error == 4) { 80 | Serial.print("Unknown error at address 0x"); 81 | if (address < 16) Serial.print("0"); 82 | Serial.println(address, HEX); 83 | } 84 | } 85 | if (num_devices == 0) 86 | Serial.println("No I2C devices found\n"); 87 | else 88 | Serial.println("done\n"); 89 | } 90 | 91 | void loop() { 92 | while (1) { 93 | vTaskDelay(portMAX_DELAY); 94 | } 95 | } 96 | 97 | void SensorWrapperLoop(void *pv_parameters) { 98 | SensorWrapper *wrapper = reinterpret_cast(pv_parameters); 99 | wrapper->Loop(); 100 | } 101 | 102 | void setup() { 103 | i2c_mutex = xSemaphoreCreateMutex(); 104 | 105 | /* Init I2C and serial communication */ 106 | Serial.begin(115200); 107 | 108 | uint8_t mac[6]; 109 | if (esp_efuse_mac_get_default(mac) != ESP_OK) { 110 | Serial.println("Failed to get MAC"); 111 | } else { 112 | Serial.printf("MAC: "); 113 | for (int i = 0; i < 6; i++) { 114 | Serial.printf("%x", mac[i]); 115 | } 116 | Serial.printf("\n"); 117 | } 118 | 119 | if (!SPIFFS.begin(true)) { 120 | Serial.println("SPIFFS failed"); 121 | esp_restart(); 122 | } 123 | 124 | { 125 | File file = SPIFFS.open("/mac.dat"); 126 | if (!file || file.isDirectory()) { 127 | Serial.println("MAC file not present."); 128 | esp_restart(); 129 | } 130 | 131 | uint8_t read_mac[6]; 132 | if (file.readBytes((char *)(read_mac), 6) < 6) { 133 | Serial.println("MAC file too small."); 134 | esp_restart(); 135 | } 136 | 137 | for (int i = 0; i < 6; i++) { 138 | if (read_mac[i] != mac[i]) { 139 | Serial.println("MAC mismatch."); 140 | esp_restart(); 141 | } 142 | } 143 | } 144 | 145 | EEPROM.begin(4096); 146 | 147 | Wire.begin(); 148 | ScanI2c(); 149 | 150 | Bmp280Sensor *bmp280 = new Bmp280Sensor(&Serial, &i2c_mutex); 151 | Scd30Sensor *scd30 = new Scd30Sensor(&Serial, &i2c_mutex, bmp280); 152 | sensors = { 153 | bmp280, 154 | scd30, 155 | new Sgp30Sensor(&Serial, &i2c_mutex, scd30, scd30), 156 | new Smuart04lSensor(&Serial, &smuart04l_serial), 157 | }; 158 | temperature_sensors = {scd30, bmp280}; 159 | 160 | for (SensorWrapper *sensor : sensors) { 161 | sensor->Init(); 162 | } 163 | 164 | for (int i = 0; i < sensors.size(); i++) { 165 | SensorWrapper *sensor = sensors[i]; 166 | xTaskCreateStaticPinnedToCore(SensorWrapperLoop, "SensorLoop", 167 | stacks[i].size(), sensor, /*priority=*/1, 168 | stacks[i].data(), &tasks[i], 1); 169 | } 170 | 171 | #ifdef WIFI_REPORTING_ENABLED 172 | xTaskCreateStaticPinnedToCore(ServerUpdateLoopMqtt, "ServerUpdateLoopMqtt", 173 | stacks[sensors.size()].size(), nullptr, 174 | /*priority=*/2, stacks[sensors.size()].data(), 175 | &tasks[sensors.size()], 1); 176 | xTaskCreateStaticPinnedToCore( 177 | ServerWatchdogLoopMqtt, "ServerWatchdogLoopMqtt", 178 | stacks[sensors.size() + 1].size(), nullptr, 179 | /*priority=*/2, stacks[sensors.size() + 1].data(), 180 | &tasks[sensors.size() + 1], 1); 181 | #endif 182 | } 183 | -------------------------------------------------------------------------------- /firmware/makemac.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sys 16 | 17 | with open('spiffs/devid.dat', 'r') as infile, open('spiffs/mac.dat', 'wb') as outfile: 18 | devid = infile.read() 19 | if len(devid) < 12: 20 | sys.exit(1) 21 | outfile.write(bytes(bytearray.fromhex(devid[:12]))) 22 | 23 | -------------------------------------------------------------------------------- /firmware/partition-table.csv: -------------------------------------------------------------------------------- 1 | # Espressif ESP32 Partition Table 2 | # Name, Type, SubType, Offset, Size, Flags 3 | nvs, data, nvs, 0x9000, 0x4000 4 | otadata, data, ota, 0xd000, 0x2000 5 | phy_init, data, phy, 0xf000, 0x1000 6 | factory, app, factory, 0x10000, 1M 7 | ota_0, app, ota_0, , 1M 8 | ota_1, app, ota_1, , 1M 9 | storage, data, spiffs, , 0xF0000, -------------------------------------------------------------------------------- /firmware/provision.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2020 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | rm -Rf spiffs/ 17 | mkdir spiffs 18 | esptool.py -p /dev/ttyUSB0 read_mac | grep MAC: | tail -n1 | sed 's/MAC: //;s/://g' > spiffs/devid.dat 19 | /usr/bin/python makemac.py 20 | DEVID=$(cat spiffs/devid.dat) 21 | KEYDIR="keys/${DEVID}" 22 | PRIVKEY="${KEYDIR}/ec_private.pem" 23 | PUBKEY="${KEYDIR}/ec_public.pem" 24 | 25 | if ! test -e "${PRIVKEY}"; then 26 | mkdir -p "${KEYDIR}" 27 | openssl ecparam -genkey -name prime256v1 -noout -out "${PRIVKEY}" 28 | openssl ec -in "${PRIVKEY}" -pubout -out "${PUBKEY}" 29 | fi 30 | 31 | openssl ec -in "${PRIVKEY}" -text -noout | grep -A3 priv: | grep -v priv: | sed -E 's/\s+//' | tr -d '\n' > spiffs/ec_private.dat 32 | 33 | if ! gcloud iot devices describe ${DEVID} --project="${PROJECT_ID}" --region="${DEVICE_REGION}" --registry="${DEVICE_REGISTRY}"; then 34 | gcloud iot devices create ${DEVID} --project="${PROJECT_ID}" --region="${DEVICE_REGION}" --registry="${DEVICE_REGISTRY}" --public-key path="${PUBKEY}",type=es256-pem 35 | fi 36 | 37 | spiffsgen.py 983040 spiffs/ spiffs.bin 38 | esptool.py -p /dev/ttyUSB0 -b 460800 write_flash --flash_mode dio --flash_size detect --flash_freq 40m 0x1000 build/bootloader/bootloader.bin 0x8000 build/partition_table/partition-table.bin 0xd000 build/ota_data_initial.bin 0x10000 build/hello-world.bin 0x310000 spiffs.bin 39 | --------------------------------------------------------------------------------