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',
+)