Initial commit of Coral Cloud Python Source

* coral-cloudiot: Support for uploading to Cloud IoT Core using MQTT and
the on-board secure element.
* coral-enviro: API and example application for the sensors, display,
and grove connectors on board the Enviro board.

Change-Id: I3da611619d3fa034ed16e9e6a7834aadfdf85e3d
diff --git a/python/coral-cloudiot/LICENSE.txt b/python/coral-cloudiot/LICENSE.txt
new file mode 100644
index 0000000..f433b1a
--- /dev/null
+++ b/python/coral-cloudiot/LICENSE.txt
@@ -0,0 +1,177 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
diff --git a/python/coral-cloudiot/coral/cloudiot/__init__.py b/python/coral-cloudiot/coral/cloudiot/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/python/coral-cloudiot/coral/cloudiot/__init__.py
diff --git a/python/coral-cloudiot/coral/cloudiot/core.py b/python/coral-cloudiot/coral/cloudiot/core.py
new file mode 100644
index 0000000..6897427
--- /dev/null
+++ b/python/coral-cloudiot/coral/cloudiot/core.py
@@ -0,0 +1,190 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Python Library for connecting to Google Cloud IoT Core via MQTT, using JWT.
+This library connects to Google Cloud IoT Core via MQTT, using a JWT for device
+authentication. After connection, publish_message can be used to provide an
+arbitrary message to a cloud project. Configuration must be done using a
+configuration file.
+"""
+
+import argparse
+import configparser
+import datetime
+import json
+import jwt
+import logging
+import os
+import paho.mqtt.client as mqtt
+import threading
+import time
+
+from coral.cloudiot.ecc608 import ecc608_jwt_with_hw_alg
+
+logger = logging.getLogger(__name__)
+
+
+class CloudIot:
+    def __init__(self, config_file, config_section='DEFAULT'):
+        self._config = configparser.ConfigParser()
+        self._config.read(config_file)
+
+        if not self._config.getboolean(config_section, 'Enabled'):
+            logger.warn('Cloud IoT is disabled per configuration.')
+            self._enabled = False
+            return
+
+        config = self._config[config_section]
+        self._project_id = config['ProjectID']
+        self._cloud_region = config['CloudRegion']
+        self._registry_id = config['RegistryID']
+        self._device_id = config['DeviceID']
+        self._ca_certs = config['CACerts']
+        self._message_type = config['MessageType']
+        self._mqtt_bridge_hostname = config['MQTTBridgeHostName']
+        self._mqtt_bridge_port = config.getint('MQTTBridgePort')
+
+        self._mutex = threading.Lock()
+
+        if ecc608_jwt_with_hw_alg:
+            # For the HW Crypto chip, use ES256. No key is needed.
+            self._algorithm = 'ES256'
+            self._private_key = None
+            self._jwt_inst = ecc608_jwt_with_hw_alg
+        else:
+            # For SW, use RS256 on a key file provided in the configuration.
+            self._algorithm = 'RS256'
+            rsa_cert = config['RSACertFile']
+            with open(rsa_cert, 'r') as f:
+                self._private_key = f.read()
+            self._jwt_inst = jwt.PyJWT()
+
+        # Create our MQTT client. The client_id is a unique string that identifies
+        # this device. For Google Cloud IoT Core, it must be in the format below.
+        self._client = mqtt.Client(
+            client_id='projects/%s/locations/%s/registries/%s/devices/%s' %
+            (self._project_id,
+             self._cloud_region,
+             self._registry_id,
+             self._device_id))
+
+        # With Google Cloud IoT Core, the username field is ignored, and the
+        # password field is used to transmit a JWT to authorize the device.
+        self._client.username_pw_set(
+            username='unused', password=self._create_jwt())
+
+        # Start thread to create new token before timeout.
+        self._term_event = threading.Event()
+        self._token_thread = threading.Thread(
+            target=self._token_update_loop, args=(self._term_event,))
+        self._token_thread.start()
+
+        # Enable SSL/TLS support.
+        self._client.tls_set(ca_certs=self._ca_certs)
+
+        # Connect to the Google MQTT bridge.
+        self._client.connect(self._mqtt_bridge_hostname,
+                             self._mqtt_bridge_port)
+
+        logger.info('Successfully connected to Cloud IoT')
+        self._enabled = True
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exception_type, exception_value, traceback):
+        if self._enabled:
+            # Terminate token thread.
+            self._term_event.set()
+            self._token_thread.join()
+
+    def enabled(self):
+        return self._enabled
+
+    def publish_message(self, message):
+        if not self._enabled:
+            return
+
+        with self._mutex:
+            # Start the network loop.
+            self._client.loop_start()
+
+            # Publish to the events or state topic based on the flag.
+            sub_topic = 'events' if self._message_type == 'event' else 'state'
+
+            mqtt_topic = '/devices/%s/%s' % (self._device_id, sub_topic)
+
+            # Publish payload using JSON dumps to create bytes representation.
+            payload = json.dumps(message)
+
+            # Publish payload to the MQTT topic. qos=1 means at least once
+            # delivery. Cloud IoT Core also supports qos=0 for at most once
+            # delivery.
+            self._client.publish(mqtt_topic, payload, qos=1)
+
+            # End the network loop and finish.
+            self._client.loop_stop()
+
+    def register_message_callbacks(self, callbacks):
+        if 'on_connect' in callbacks:
+            self._client.on_connect = callbacks['on_connect']
+        if 'on_disconnect' in callbacks:
+            self._client.on_disconnect = callbacks['on_disconnect']
+        if 'on_publish' in callbacks:
+            self._client.on_publish = callbacks['on_publish']
+        if 'on_message' in callbacks:
+            self._client.on_message = callbacks['on_message']
+        if 'on_unsubscribe' in callbacks:
+            self._client.on_unsubscribe = callbacks['on_unsubscribe']
+        if 'on_log' in callbacks:
+            self._client.on_log = callbacks['on_log']
+
+    def _token_update_loop(self, term_event):
+        # Update token every 50 minutes (of allowed 60).
+        while not term_event.wait(50 * 60):
+            with self._mutex:
+                self._client.disconnect()
+
+                # Set new token.
+                self._client.username_pw_set(
+                    username='unused', password=self._create_jwt())
+
+                # Connect to the Google MQTT bridge.
+                self._client.connect(
+                    self._mqtt_bridge_hostname, self._mqtt_bridge_port)
+
+                logger.info(
+                    'Successfully re-established connection with new token')
+
+    def _create_jwt(self):
+        """Creates a JWT (https://jwt.io) to establish an MQTT connection.
+            Args:
+                Project_id: The cloud project ID this device belongs to
+                 algorithm: The encryption algorithm to use. Either 'RS256' or 'ES256'
+            Returns:
+                An MQTT generated from the given project_id and private key, which
+                expires in 20 minutes. After 20 minutes, your client will be
+                disconnected, and a new JWT will have to be generated.
+        """
+
+        token = {
+            # The time that the token was issued at
+            'iat': datetime.datetime.utcnow(),
+            # The time the token expires.
+            'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=60),
+            # The audience field should always be set to the GCP project id.
+            'aud': self._project_id
+        }
+
+        return self._jwt_inst.encode(token, self._private_key, algorithm=self._algorithm)
diff --git a/python/coral-cloudiot/coral/cloudiot/ecc608.py b/python/coral-cloudiot/coral/cloudiot/ecc608.py
new file mode 100644
index 0000000..67cb9d4
--- /dev/null
+++ b/python/coral-cloudiot/coral/cloudiot/ecc608.py
@@ -0,0 +1,154 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import json
+import datetime
+import logging
+import os
+import sys
+import jwt
+from cryptography.exceptions import InvalidSignature
+from cryptography.hazmat.primitives import hashes
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptoauthlib import *
+
+logger = logging.getLogger(__name__)
+
+def _split_equal_parts(line, n):
+    return [line[i:i+n] for i in range(0, len(line), n)]
+
+def _ascii_hex_string(a, l=16):
+    """
+    Format a bytearray object into a formatted ascii hex string
+    """
+    return '\n'.join(x.hex().upper() for x in _split_equal_parts(a, l))
+
+
+def _convert_ec_pub_to_pem(raw_pub_key):
+    """
+    Convert to the key to PEM format. Expects bytes
+    """
+    public_key_der = bytearray.fromhex(
+        '3059301306072A8648CE3D020106082A8648CE3D03010703420004') + raw_pub_key
+    public_key_b64 = base64.b64encode(public_key_der).decode('ascii')
+    public_key_pem = '-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----' % '\n'.join(_split_equal_parts(public_key_b64, 64))
+    return public_key_pem
+
+
+def _ecc608_check_address(address):
+    cfg = cfg_ateccx08a_i2c_default()
+    # Cryptolib uses 8-bit address.
+    cfg.cfg.atcai2c.slave_address = address << 1
+    cfg.cfg.atcai2c.bus = 1  # ARM I2C
+    cfg.devtype = 3  # ECC608
+    status = atcab_init(cfg)
+    if status == 0:
+        return True
+    return False
+
+
+def ecc608_find_chip():
+    """Returns the I2C address of the crypto chip or None if chip is not installed."""
+
+    for addr in (0x30, 0x60, 0x62):
+        if _ecc608_check_address(addr):
+            logger.info('Found crypto chip at 0x%x', addr)
+            return addr
+    logger.warning('No crypto detected, using SW.')
+    return None
+
+
+def ecc608_hw_sign(msg, key_id=0):
+    digest = bytearray(32)
+    status = atcab_sha(len(msg), msg, digest)
+    assert status == 0
+
+    signature = bytearray(64)
+    status = atcab_sign(key_id, digest, signature)
+    assert status == 0
+    return signature
+
+
+def ecc608_man_jwt(claims):
+    header = '{"typ":"JWT","alg":"ES256"}'
+
+    for k, v in claims.items():
+        if type(v) is datetime.datetime:
+            claims[k] = int(v.timestamp())
+
+    payload = json.dumps(claims)
+
+    token = base64.urlsafe_b64encode(
+        header.encode('ascii')).replace(b'=', b'') + b'.'
+
+    token = token + \
+        base64.urlsafe_b64encode(payload.encode('ascii')).replace(b'=', b'')
+
+    signature = ecc608_hw_sign(token)
+
+    token = token + b'.' + \
+        base64.urlsafe_b64encode(signature).replace(b'=', b'')
+    return token
+
+
+def ecc608_serial():
+    serial = bytearray(9)
+    assert atcab_read_serial_number(serial) == 0
+    return _ascii_hex_string(serial)
+
+
+def ecc608_public_key(key_id=0):
+    public_key = bytearray(64)
+    status = atcab_get_pubkey(key_id, public_key)
+    assert status == 0
+    return _convert_ec_pub_to_pem(public_key)
+
+
+class HwEcAlgorithm(jwt.algorithms.Algorithm):
+    def __init__(self):
+        self.hash_alg = hashes.SHA256
+
+    def prepare_key(self, key):
+        return key
+
+    def sign(self, msg, key):
+        return ecc608_hw_sign(msg)
+
+    def verify(self, msg, key, sig):
+        try:
+            der_sig = jwt.utils.raw_to_der_signature(sig, key.curve)
+        except ValueError:
+            return False
+
+        try:
+            key.verify(der_sig, msg, ec.ECDSA(self.hash_alg()))
+            return True
+        except InvalidSignature:
+            return False
+
+
+# On module import, load library.
+try:
+    ecc608_i2c_address = None
+    ecc608_jwt_with_hw_alg = None
+
+    load_cryptoauthlib()
+
+    ecc608_i2c_address = ecc608_find_chip()
+    if ecc608_i2c_address is not None:
+        ecc608_jwt_with_hw_alg = jwt.PyJWT(algorithms=[])
+        ecc608_jwt_with_hw_alg.register_algorithm('ES256', HwEcAlgorithm())
+except Exception:
+    logger.warning('Unable to load HW crypto library, using SW.')
diff --git a/python/coral-cloudiot/coral/cloudiot/ecc608_pubkey.py b/python/coral-cloudiot/coral/cloudiot/ecc608_pubkey.py
new file mode 100755
index 0000000..f54c5e5
--- /dev/null
+++ b/python/coral-cloudiot/coral/cloudiot/ecc608_pubkey.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+#
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import base64
+import sys
+
+from ecc608 import ecc608_i2c_address
+from ecc608 import ecc608_serial
+from ecc608 import ecc608_public_key
+
+
+def main():
+    if ecc608_i2c_address is None:
+        return 1
+
+    print('Serial Number: %s\n\n' % ecc608_serial())
+
+    print(ecc608_public_key())
+
+    return 0
+
+
+if __name__ == '__main__':
+    sys.exit(main())
diff --git a/python/coral-cloudiot/setup.py b/python/coral-cloudiot/setup.py
new file mode 100644
index 0000000..797381d
--- /dev/null
+++ b/python/coral-cloudiot/setup.py
@@ -0,0 +1,22 @@
+from setuptools import setup, find_packages
+
+setup(
+    name='coral-cloudiot',
+    version='1.0',
+    description='Coral Cloud IoT API',
+    author='Coral Team',
+    author_email='coral-support@google.com',
+    url="https://coral.withgoogle.com/",
+    project_urls={
+        'Repo': 'https://coral.googlesource.com/coral-cloud/',
+        'Issues': 'https://github.com/google-coral/issues',
+    },
+    license='Apache 2',
+    packages=['coral.cloudiot'],
+    install_requires=[
+        'jwt',
+        'paho-mqtt',
+        'cryptoauthlib',
+    ],
+    python_requires='>=3.5.3',
+)
diff --git a/python/coral-enviro/LICENSE.txt b/python/coral-enviro/LICENSE.txt
new file mode 100644
index 0000000..f433b1a
--- /dev/null
+++ b/python/coral-enviro/LICENSE.txt
@@ -0,0 +1,177 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
diff --git a/python/coral-enviro/MANIFEST.in b/python/coral-enviro/MANIFEST.in
new file mode 100644
index 0000000..4ff6b05
--- /dev/null
+++ b/python/coral-enviro/MANIFEST.in
@@ -0,0 +1 @@
+include coral/enviro/my_config.ini
diff --git a/python/coral-enviro/coral/enviro/__init__.py b/python/coral-enviro/coral/enviro/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/python/coral-enviro/coral/enviro/__init__.py
diff --git a/python/coral-enviro/coral/enviro/board.py b/python/coral-enviro/coral/enviro/board.py
new file mode 100644
index 0000000..38431b1
--- /dev/null
+++ b/python/coral-enviro/coral/enviro/board.py
@@ -0,0 +1,89 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Drivers for shared functionality provided by the Environment Bonnet."""
+
+import os
+from luma.core.interface.serial import noop, spi
+from luma.oled.device import ssd1306
+
+
+def _get_path(sysfs_name):
+    search_path = '/sys/bus/iio/devices/'
+    try:
+        for fname in os.listdir(search_path):
+            with open(search_path + fname + '/name', 'r') as f:
+                if sysfs_name in f.read():
+                    return search_path + fname
+        return ''
+    except FileNotFoundError:
+        return ''
+
+
+def _read_sysfs(path, retries=2):
+    try:
+        with open(path, 'r') as f:
+            # Allow multiple attempts in case sensor times out.
+            for _ in range(retries):
+                try:
+                    data = f.read()
+                    if data:
+                        return float(data)
+                except:
+                    pass
+            return None
+    except FileNotFoundError:
+        return None
+
+class EnviroBoard():
+    def __init__(self):
+        # Obtain the full sysfs path of the IIO devices.
+        self._hdc2010 = _get_path('hdc20x0')
+        self._bmp280 = _get_path('bmp280')
+        self._opt3002 = _get_path('opt3001')
+        self._tla2021 = _get_path('ads1015')
+        # Create SSD1306 OLED instance, with SPI as the interface.
+        self._display = ssd1306(serial_interface=spi(),
+                                gpio=noop(), height=32, rotate=2)
+
+    @property
+    def temperature(self):
+        temperature = _read_sysfs(self._hdc2010 + '/in_temp_input')
+        if temperature is not None:
+            return temperature
+        temperature = _read_sysfs(self._bmp280 + '/in_temp_input')
+        # BMP280 reports as mC.
+        if temperature is not None:
+            return temperature / 1000.0
+        return None
+
+    @property
+    def humidity(self):
+        return _read_sysfs(self._hdc2010 + '/in_humidityrelative_input')
+
+    @property
+    def ambient_light(self):
+        return _read_sysfs(self._opt3002 + '/in_illuminance_input')
+
+    @property
+    def pressure(self):
+        return _read_sysfs(self._bmp280 + '/in_pressure_input')
+
+    @property
+    def grove_analog(self):
+        return _read_sysfs(self._tla2021 + '/in_voltage0_raw')
+
+    @property
+    def display(self):
+        return self._display
diff --git a/python/coral-enviro/coral/enviro/enviro_demo.py b/python/coral-enviro/coral/enviro/enviro_demo.py
new file mode 100644
index 0000000..75bfcda
--- /dev/null
+++ b/python/coral-enviro/coral/enviro/enviro_demo.py
@@ -0,0 +1,73 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from coral.enviro.board import EnviroBoard
+from coral.cloudiot.core import CloudIot
+from luma.core.render import canvas
+from PIL import ImageDraw
+from time import sleep
+
+import argparse
+import itertools
+
+
+def update_display(display, msg):
+    with canvas(display) as draw:
+        draw.text((0, 0), msg, fill='white')
+
+
+def _none_to_nan(val):
+    return float('nan') if val is None else val
+
+
+def main():
+    # Pull arguments from command line.
+    parser = argparse.ArgumentParser(description='Enviro Kit Demo')
+    parser.add_argument('--display_duration',
+                        help='Measurement display duration (seconds)', type=int,
+                        default=5)
+    parser.add_argument('--upload_delay', help='Cloud upload delay (seconds)',
+                        type=int, default=300)
+    parser.add_argument(
+        '--cloud_config', help='Cloud IoT config file', default='my_config.ini')
+    args = parser.parse_args()
+
+    # Create instances of EnviroKit and Cloud IoT.
+    enviro = EnviroBoard()
+    with CloudIot(args.cloud_config) as cloud:
+        # Indefinitely update display and upload to cloud.
+        sensors = {}
+        read_period = int(args.upload_delay / (2 * args.display_duration))
+        for read_count in itertools.count():
+            # First display temperature and RH.
+            sensors['temperature'] = enviro.temperature
+            sensors['humidity'] = enviro.humidity
+            msg = 'Temp: %.2f C\n' % _none_to_nan(sensors['temperature'])
+            msg += 'RH: %.2f %%' % _none_to_nan(sensors['humidity'])
+            update_display(enviro.display, msg)
+            sleep(args.display_duration)
+            # After 5 seconds, switch to light and pressure.
+            sensors['ambient_light'] = enviro.ambient_light
+            sensors['pressure'] = enviro.pressure
+            msg = 'Light: %.2f lux\n' % _none_to_nan(sensors['ambient_light'])
+            msg += 'Pressure: %.2f kPa' % _none_to_nan(sensors['pressure'])
+            update_display(enviro.display, msg)
+            sleep(args.display_duration)
+            # If time has elapsed, attempt cloud upload.
+            if read_count % read_period == 0 and cloud.enabled():
+                cloud.publish_message(sensors)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/python/coral-enviro/coral/enviro/my_config.ini b/python/coral-enviro/coral/enviro/my_config.ini
new file mode 100644
index 0000000..96cfd45
--- /dev/null
+++ b/python/coral-enviro/coral/enviro/my_config.ini
@@ -0,0 +1,14 @@
+[DEFAULT]
+Enabled = false
+ProjectID = my-project
+CloudRegion = us-central1
+RegistryID = enviro-kit
+DeviceID = enviro-kit
+# CA Certs should be pulled from https://pki.goog/roots.pem
+CACerts = roots.pem
+MQTTBridgeHostName = mqtt.googleapis.com
+MQTTBridgePort = 8883
+# MessageType is expected to always be event.
+MessageType = event
+# RSA Cert is not required unless SW crypto is used.
+RSACertFile =
diff --git a/python/coral-enviro/setup.py b/python/coral-enviro/setup.py
new file mode 100644
index 0000000..5de3e79
--- /dev/null
+++ b/python/coral-enviro/setup.py
@@ -0,0 +1,23 @@
+from setuptools import setup, find_packages
+
+setup(
+    name='coral-enviro',
+    version='1.0',
+    description='API and Demo Application for Coral Environmental Sensor Board',
+    author='Coral Team',
+    author_email='coral-support@google.com',
+    url="https://coral.withgoogle.com/",
+    project_urls={
+        'Repo': 'https://coral.googlesource.com/coral-cloud/',
+        'Issues': 'https://github.com/google-coral/issues',
+    },
+    license='Apache 2',
+    packages=['coral.enviro'],
+    include_package_data=True,
+    install_requires=[
+        'luma.oled>=2.3.2',
+        'spidev',
+        'coral-cloudiot',
+    ],
+    python_requires='>=3.5.3',
+)