diff --git a/configuration-service/README.md b/configuration-service/README.md
index efba33d..3de7687 100644
--- a/configuration-service/README.md
+++ b/configuration-service/README.md
@@ -1,7 +1,5 @@
# Eddystone Configuration GATT Service
-**Note: https://github.com/google/eddystone/issues/132 needs resolution before this specification can be considered final.**
-
This document defines the specification for the Eddystone Configuration Service and offers some implementation guidance.
The Eddystone Configuration Service runs as a GATT service on the beacon while it is connectable and allows configuration of the advertised data, the broadcast power levels, and the advertising intervals. It also forms part of the definition of how Eddystone-EID beacons are configured and registered with a trusted resolver.
@@ -12,20 +10,23 @@ The primary goal of a standardized configuration service is interoperability bet
There are two ways to accomplish this. Your choice depends on whether your beacons are always connectable, or if they are non-connectable by default and become connectable through some action by the user.
-We recommend that your beacons spend the majority of their lives in a non-connectable state, which avoids network congestion and collision issues in a dense wireless environment. In this case, the user will take some action to make a beacon temporarily connectable, perhaps by depressing a button, pulling a battery activation tab, or similar. Once your beacon becomes connectable, we recommend broadcasting the configuration GATT service's UUID as a 128-bit service UUID in the scan response, along with a local name data type that identifies your beacon. Configuration applications may look for both of these signals in the scan records of nearby beacons and use the information to highlight which ones are ready for configuration. (A flash of an LED or an audible beep upon connection from a client are helpful too, if your hardware has the necessary support.)
+We recommend that your beacons spend the majority of their lives in a non-connectable state, which avoids network congestion and collision issues in a dense wireless environment. In this case, the user will take some action to make a beacon temporarily connectable, perhaps by depressing a button, pulling a battery activation tab, or similar. The connectable timeout should be at the very least 30 seconds to let enough time to user to start a configuration application. Once your beacon becomes connectable, we recommend broadcasting the configuration GATT service's UUID as a 128-bit service UUID in the scan response, along with a local name data type that identifies your beacon. Configuration applications may look for both of these signals in the scan records of nearby beacons and use the information to highlight which ones are ready for configuration. (A flash of an LED or an audible beep upon connection from a client are helpful too, if your hardware has the necessary support.)
-If your beacons must be always connectable, you may wish to minimize the amount of data that's broadcast in a scan response to conserve the battery. In this case we recommend that in an occasional scan response you broadcast a short local name so that configuration clients can display it to the user and aid an easier association between your beacon and any other nearby beacons. The time between scan responses should be tuned to balance the power requirements with the patience of a human trying to find a connectable frame to configure your beacon -- once the hardware device address has been obtained it should be possible to connect to the beacon in a few seconds.
+If your beacons must be always connectable, you may wish to minimize the amount of data that's broadcast in a scan response to conserve the battery. In this case we recommend that in an occasional scan response you broadcast a short local name so that configuration clients can display it to the user and aid an easier association between your beacon and any other nearby beacons. The time between scan responses should be tuned to balance the power requirements with the patience of a human trying to find a connectable frame to configure your beacon -- once the hardware device address has been obtained it should be possible to connect to the beacon in a few seconds. (Here too, a flash of an LED or an audible beep upon connection from a client may be helpful, if your hardware has the necessary support, especially if there are multiple connectable beacons in the vicinity.)
In either case, users may find it helpful if your beacons arrive broadcasting one of the following two frames:
* An Eddystone-URL frame type whose URL points to information about using your beacons. We recommend this for ease of recognition by the person who is configuring the device.
* An Eddystone-UID frame type where the namespace part of the UID is fixed to the one you've chosen for your organization and the instance part is randomized. This ensures the beacon is ready to be registered with a service that supports the UID format with no configuration necessary.
+### Default unlock code
+If a beacon ships in a `0x02` unlocked state, care must be taken to ensure that it has a default unlock code that's known to the owner.
+
### Single connection only
While a configuration client is connected to the beacon, the beacon must refuse other connections.
### Interleaving multiple advertisement frames.
-If variable advertising intervals are supported (see `IS_VARIABLE_ADV_SUPPORTED` in the Broadcast Capabilities characteristic) it will be inevitable that some slots will be scheduled to broadcast at the same time. We recommend that implementers transmit those frames in slot order at the shortest permissible advertising interval (100 ms).
+If variable advertising intervals are supported (see `IS_VARIABLE_ADV_SUPPORTED` in the Capabilities characteristic) it will be inevitable that some slots will be scheduled to broadcast at the same time. We recommend that implementers transmit those frames in slot order at the shortest permissible advertising interval (100 ms).
## Specification
@@ -43,7 +44,7 @@ If variable advertising intervals are supported (see `IS_VARIABLE_ADV_SUPPORTED`
Notes
-
Where not explicitly stated, data written and read is defined in terms of big-endian arrays of signed bytes.
+
Where not explicitly stated, data written and read is defined in terms of big-endian arrays of bytes. However, there are exceptions for EID related keys: the Public ECDH Key in Characteristic 8, the Identity Key in Characteristic 9, and the service's public ECDH key when writing an EID slot in Characteristic 10, all three of which are little-endian. This exception exists because the reference design for eliptic curve-25519, which has been widely adopted, was implemented using little-endian arithmetic and is the basis for the EID design. Note: the elliptic curve-25519 reference design is little-endian, even though big-endian is used almost universally for all other cryptographic protocols
@@ -377,8 +378,8 @@ Writeable only in locked state.
Return Codes
-Read Not Permitted: for any attempt to read or write while the beacon is locked.
-Write Not Permitted: for any attempt to write while the beacon is locked.
+Read Not Permitted: for any attempt to read while the beacon is unlocked.
+Write Not Permitted: for any attempt to write while the beacon is unlocked.
@@ -406,7 +407,7 @@ Length: 32 bytes
Description
-Reads the beacon's 256-bit public ECDH key.
+Reads the beacon's 256-bit public ECDH key (little-endian).
@@ -444,7 +445,7 @@ Length: 16 bytes
Description
-Reads the identity key for the active slot. If the slot isn't configured as EID, returns an error.
+Reads the identity key (little-endian) for the active slot. If the slot isn't configured as EID, returns an error.
To prevent this data being broadcast in the clear, it shall be AES-128 encrypted with the lock code for the beacon.
@@ -469,7 +470,7 @@ Read Not Permitted: for any attempt to read while the beacon is locked.
Characteristic UUID
-
a3c8750A-8ed3-4bdf-8a39-a01bebede295
+
a3c8750a-8ed3-4bdf-8a39-a01bebede295
Properties
@@ -504,7 +505,7 @@ In the case of UID and URL frames, the data to be broadcast is supplied in the a
In the case of a TLM frame, the data is just the frame type byte, 0x20. If another slot on the beacon has been configured as an EID frame type, the beacon shall broadcast the ETLM variety of telemetry. Otherwise, the plain TLM frame shall be broadcast. If the beacon is currently broadcasting a plain TLM frame and an EID frame is configured, the beacon shall switch to broadcasting the ETLM variety. If the beacon is configured to broadcast multiple EID frames, then the beacon should cycle through the set identity keys and use them in turn to broadcast an equal number of ETLM frames.
-In the case of an EID frame, the length is either 33 or 17. If 33, it's the 32-byte service's public ECDH key and the exponent byte. This is the prefered method of provisioning an EID beacon. If 17, it's the result of encrypting the 16-byte identity key with the beacon's lock code, and the exponent. This is less secure and any provisioner who implements this should make it clear to the user.
+In the case of an EID frame, the length is either 34 or 18. If 34, it's the frame type, the 32-byte service's public ECDH key (little-endian) and the exponent byte. This is the prefered method of provisioning an EID beacon. If 18, it's the frame type, the result of encrypting the 16-byte identity key (little-endian) with the beacon's lock code (big-endian), and the exponent. This is less secure and any provisioner who implements this should make it clear to the user.
Writing an empty array, or a single 0x00 byte clears the slot and stops Tx. If configured as an EID beacon this will also destroy the peripheral's state for this frame data.
@@ -531,7 +532,7 @@ Invalid Attribute Length: on any attempt to write with an illegal number of byte
Characteristic UUID
-
a3c8750B-8ed3-4bdf-8a39-a01bebede295
+
a3c8750b-8ed3-4bdf-8a39-a01bebede295
Properties
@@ -546,7 +547,7 @@ If a value of 0x0B is written, the beacon shall reset to its factor
Any other value shall be ignored.
-In addition, any write shall be ignored if the lock state is not 0x01, i.e. locked and automatic lock disabled. The beacon must have been purposefully unlocked by the current client before a factory reset can be performed.
+In addition, any write shall be ignored if the lock state is not 0x01. The beacon must have been purposefully unlocked by the current client before a factory reset can be performed.
@@ -567,7 +568,7 @@ In addition, any write shall be ignored if the lock state is not 0x01
Characteristic UUID
-
a3c8750C-8ed3-4bdf-8a39-a01bebede295
+
a3c8750c-8ed3-4bdf-8a39-a01bebede295
Properties
diff --git a/eddystone-uid/tools/txeddystone-uid/TxEddystone-UID/app/src/main/AndroidManifest.xml b/eddystone-uid/tools/txeddystone-uid/TxEddystone-UID/app/src/main/AndroidManifest.xml
index 8c61d92..b083f55 100644
--- a/eddystone-uid/tools/txeddystone-uid/TxEddystone-UID/app/src/main/AndroidManifest.xml
+++ b/eddystone-uid/tools/txeddystone-uid/TxEddystone-UID/app/src/main/AndroidManifest.xml
@@ -13,7 +13,8 @@
android:theme="@style/AppTheme">
+ android:label="@string/app_name"
+ android:screenOrientation="portrait">
diff --git a/eddystone-uid/tools/txeddystone-uid/TxEddystone-UID/app/src/main/java/com/google/sample/txeddystone_uid/MainActivity.java b/eddystone-uid/tools/txeddystone-uid/TxEddystone-UID/app/src/main/java/com/google/sample/txeddystone_uid/MainActivity.java
index 09047d4..59d9517 100644
--- a/eddystone-uid/tools/txeddystone-uid/TxEddystone-UID/app/src/main/java/com/google/sample/txeddystone_uid/MainActivity.java
+++ b/eddystone-uid/tools/txeddystone-uid/TxEddystone-UID/app/src/main/java/com/google/sample/txeddystone_uid/MainActivity.java
@@ -351,7 +351,7 @@ private byte[] buildServiceData() throws IOException {
}
private boolean isValidHex(String s, int len) {
- return !(s == null || s.isEmpty()) && (s.length() / 2) == len && s.matches("[0-9A-F]+");
+ return !(s == null || s.isEmpty()) && s.length() == len*2 && s.matches("[0-9A-F]+");
}
private byte[] toByteArray(String hexString) {
diff --git a/eddystone-url/README.md b/eddystone-url/README.md
index 00556e1..eecc4a9 100644
--- a/eddystone-url/README.md
+++ b/eddystone-url/README.md
@@ -13,7 +13,7 @@ Byte offset | Field | Description
0 | Frame Type | Value = `0x10`
1 | TX Power | Calibrated Tx power at 0 m
2 | URL Scheme | Encoded Scheme Prefix
-3+ | Encoded URL | Length 0-17
+3+ | Encoded URL | Length 1-17
### Tx Power Level
diff --git a/eddystone-url/implementations/Arduino/README.md b/eddystone-url/implementations/Arduino/README.md
new file mode 100644
index 0000000..e51af69
--- /dev/null
+++ b/eddystone-url/implementations/Arduino/README.md
@@ -0,0 +1,3 @@
+# Arduino (BLEPeripheral)
+
+[Arduino (BLEPeripheral)](https://github.com/sandeepmistry/arduino-BLEPeripheral/blob/master/examples/Eddystone/EddystoneURL/EddystoneURL.ino), a list of compatible hardware can be found [here](https://github.com/sandeepmistry/arduino-BLEPeripheral#compatible-hardware).
\ No newline at end of file
diff --git a/eddystone-url/implementations/BBC-microbit/README.md b/eddystone-url/implementations/BBC-microbit/README.md
new file mode 100644
index 0000000..0224ad4
--- /dev/null
+++ b/eddystone-url/implementations/BBC-microbit/README.md
@@ -0,0 +1,3 @@
+# BBC micro:bit
+
+[microbit-physicalweb](https://github.com/showio/microbit-physicalweb)
diff --git a/eddystone-url/implementations/PyBeacon/LICENSE b/eddystone-url/implementations/PyBeacon/LICENSE
new file mode 100644
index 0000000..8dada3e
--- /dev/null
+++ b/eddystone-url/implementations/PyBeacon/LICENSE
@@ -0,0 +1,201 @@
+ 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
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ 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.
diff --git a/eddystone-url/implementations/PyBeacon/PyBeacon/PyBeacon.py b/eddystone-url/implementations/PyBeacon/PyBeacon/PyBeacon.py
new file mode 100644
index 0000000..0c0b3c2
--- /dev/null
+++ b/eddystone-url/implementations/PyBeacon/PyBeacon/PyBeacon.py
@@ -0,0 +1,296 @@
+#!/usr/bin/env python3
+#
+# Copyright 2015 Opera Software ASA. All rights reserved.
+#
+# 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 script for scanning and advertising urls over Eddystone-URL.
+"""
+import re
+import os
+import signal
+import subprocess
+import sys
+import time
+import argparse
+from pprint import pprint
+
+application_name = 'PyBeacon'
+version = '0.2.4.3beta'
+
+if (sys.version_info > (3, 0)):
+ DEVNULL = subprocess.DEVNULL
+else:
+ DEVNULL = open(os.devnull, 'wb')
+
+# The default url
+url = "https://goo.gl/SkcDTN"
+
+schemes = [
+ "http://www.",
+ "https://www.",
+ "http://",
+ "https://",
+ ]
+
+extensions = [
+ ".com/", ".org/", ".edu/", ".net/", ".info/", ".biz/", ".gov/",
+ ".com", ".org", ".edu", ".net", ".info", ".biz", ".gov",
+ ]
+
+parser = argparse.ArgumentParser(prog=application_name, description= __doc__)
+
+parser.add_argument("-u", "--url", nargs='?', const=url, type=str,
+ default=url, help='URL to advertise.')
+parser.add_argument('-s','--scan', action='store_true',
+ help='Scan for URLs.')
+parser.add_argument('-t','--terminate', action='store_true',
+ help='Stop advertising URL.')
+parser.add_argument('-o','--one', action='store_true',
+ help='Scan one URL only.')
+parser.add_argument("-v", "--version", action='store_true',
+ help='Version of ' + application_name + '.')
+parser.add_argument("-V", "--verbose", action='store_true',
+ help='Print lots of debug output.')
+
+args = parser.parse_args()
+
+def verboseOutput(text = ""):
+ if args.verbose:
+ sys.stderr.write(text + "\n")
+
+
+def decodeUrl(encodedUrl):
+ """
+ Decode a url encoded with the Eddystone (or UriBeacon) URL encoding scheme
+ """
+
+ decodedUrl = schemes[encodedUrl[0]]
+ for c in encodedUrl[1:]:
+ if c <= 0x20:
+ decodedUrl += extensions[c]
+ else:
+ decodedUrl += chr(c)
+
+ return decodedUrl
+
+
+def onUrlFound(url):
+ """
+ Called by onPacketFound, if the packet contains a url.
+ """
+
+ sys.stdout.write(url)
+ sys.stdout.write("\n")
+ sys.stdout.flush()
+
+
+foundPackets = set()
+
+def onPacketFound(packet):
+ """
+ Called by the scan function for each beacon packets found.
+ """
+
+ data = bytearray.fromhex(packet)
+
+ if args.one:
+ tmp = packet[:-3]
+ if tmp in foundPackets:
+ return
+ foundPackets.add(tmp)
+
+ # Eddystone
+ if len(data) >= 20 and data[19] == 0xaa and data[20] == 0xfe:
+ serviceDataLength = data[21]
+ frameType = data[25]
+
+ # Eddystone-URL
+ if frameType == 0x10:
+ verboseOutput("Eddystone-URL")
+ onUrlFound(decodeUrl(data[27:22 + serviceDataLength]))
+ elif frameType == 0x00:
+ verboseOutput("Eddystone-UID")
+ elif frameType == 0x20:
+ verboseOutput("Eddystone-TLM")
+ else:
+ verboseOutput("Unknown Eddystone frame type: {}".format(frameType))
+
+ # UriBeacon
+ elif len(data) >= 20 and data[19] == 0xd8 and data[20] == 0xfe:
+ serviceDataLength = data[21]
+ verboseOutput("UriBeacon")
+ onUrlFound(decodeUrl(data[27:22 + serviceDataLength]))
+
+ else:
+ verboseOutput("Unknown beacon type")
+
+ verboseOutput(packet)
+ verboseOutput()
+
+
+def scan(duration = None):
+ """
+ Scan for beacons. This function scans for [duration] seconds. If duration
+ is set to None, it scans until interrupted.
+ """
+ print("Scanning...")
+ subprocess.call("sudo hciconfig hci0 reset", shell = True, stdout = DEVNULL)
+
+ lescan = subprocess.Popen(
+ ["sudo", "-n", "hcitool", "lescan", "--duplicates"],
+ stdout = DEVNULL)
+
+ dump = subprocess.Popen(
+ ["sudo", "-n", "hcidump", "--raw"],
+ stdout = subprocess.PIPE)
+
+ packet = None
+ try:
+ startTime = time.time()
+ for line in dump.stdout:
+ line = line.decode()
+ if line.startswith("> "):
+ if packet: onPacketFound(packet)
+ packet = line[2:].strip()
+ elif line.startswith("< "):
+ if packet: onPacketFound(packet)
+ packet = None
+ else:
+ if packet: packet += " " + line.strip()
+
+ if duration and time.time() - startTime > duration:
+ break
+
+ except KeyboardInterrupt:
+ pass
+
+ subprocess.call(["sudo", "kill", str(dump.pid), "-s", "SIGINT"])
+ subprocess.call(["sudo", "-n", "kill", str(lescan.pid), "-s", "SIGINT"])
+
+
+def encodeurl(url):
+ i = 0
+ data = []
+
+ for s in range(len(schemes)):
+ scheme = schemes[s]
+ if url.startswith(scheme):
+ data.append(s)
+ i += len(scheme)
+ break
+ else:
+ raise Exception("Invalid url scheme")
+
+ while i < len(url):
+ if url[i] == '.':
+ for e in range(len(extensions)):
+ expansion = extensions[e]
+ if url.startswith(expansion, i):
+ data.append(e)
+ i += len(expansion)
+ break
+ else:
+ data.append(0x2E)
+ i += 1
+ else:
+ data.append(ord(url[i]))
+ i += 1
+
+ return data
+
+
+def encodeMessage(url):
+ encodedurl = encodeurl(url)
+ encodedurlLength = len(encodedurl)
+
+ verboseOutput("Encoded url length: " + str(encodedurlLength))
+
+ if encodedurlLength > 18:
+ raise Exception("Encoded url too long (max 18 bytes)")
+
+ message = [
+ 0x02, # Flags length
+ 0x01, # Flags data type value
+ 0x1a, # Flags data
+
+ 0x03, # Service UUID length
+ 0x03, # Service UUID data type value
+ 0xaa, # 16-bit Eddystone UUID
+ 0xfe, # 16-bit Eddystone UUID
+
+ 5 + len(encodedurl), # Service Data length
+ 0x16, # Service Data data type value
+ 0xaa, # 16-bit Eddystone UUID
+ 0xfe, # 16-bit Eddystone UUID
+
+ 0x10, # Eddystone-url frame type
+ 0xed, # txpower
+ ]
+
+ message += encodedurl
+
+ return message
+
+
+def advertise(url):
+ print("Advertising: " + url)
+ message = encodeMessage(url)
+
+ # Prepend the length of the whole message
+ message.insert(0, len(message))
+
+ # Pad message to 32 bytes for hcitool
+ while len(message) < 32: message.append(0x00)
+
+ # Make a list of hex strings from the list of numbers
+ message = map(lambda x: "%02x" % x, message)
+
+ # Concatenate all the hex strings, separated by spaces
+ message = " ".join(message)
+ verboseOutput("Message: " + message)
+
+ subprocess.call("sudo hciconfig hci0 up", shell = True, stdout = DEVNULL)
+
+ # Stop advertising
+ subprocess.call("sudo hcitool -i hci0 cmd 0x08 0x000a 00", shell = True, stdout = DEVNULL)
+
+ # Set message
+ subprocess.call("sudo hcitool -i hci0 cmd 0x08 0x0008 " + message, shell = True, stdout = DEVNULL)
+
+ # Resume advertising
+ subprocess.call("sudo hcitool -i hci0 cmd 0x08 0x000a 01", shell = True, stdout = DEVNULL)
+
+
+def stopAdvertising():
+ print("Stopping advertising")
+ subprocess.call("sudo hcitool -i hci0 cmd 0x08 0x000a 00", shell = True, stdout = DEVNULL)
+
+def showVersion():
+ print(application_name + " " + version)
+
+def main():
+ subprocess.call(["sudo", "-v"])
+ if args.version:
+ showVersion()
+ elif args.terminate:
+ stopAdvertising()
+ elif args.one:
+ scan(3)
+ elif args.scan:
+ scan()
+ else:
+ advertise(args.url)
+
+if __name__ == "__main__":
+ main()
diff --git a/eddystone-url/implementations/PyBeacon/PyBeacon/__init__.py b/eddystone-url/implementations/PyBeacon/PyBeacon/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/eddystone-url/implementations/PyBeacon/PyBeacon/__main__.py b/eddystone-url/implementations/PyBeacon/PyBeacon/__main__.py
new file mode 100644
index 0000000..0c0ffd0
--- /dev/null
+++ b/eddystone-url/implementations/PyBeacon/PyBeacon/__main__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from .RPyBeacon import main
+main()
\ No newline at end of file
diff --git a/eddystone-url/implementations/PyBeacon/README.md b/eddystone-url/implementations/PyBeacon/README.md
new file mode 100644
index 0000000..622df9b
--- /dev/null
+++ b/eddystone-url/implementations/PyBeacon/README.md
@@ -0,0 +1,30 @@
+# PyBeacon
+Python script for scanning and advertising urls over Eddystone-URL.
+
+**Note**: This is just a stable build. For latest builds, check [Nirmanakarta/PyBeacon](https://github.com/nirmankarta/PyBeacon).
+
+## Requirements
+
+* Python 3.x (Scanning will not work on Python 2.x)
+* Bluez
+ * sudo apt-get install bluez bluez-hcidump
+
+## Installation
+
+ sudo pip install PyBeacon
+
+## Upgrade
+
+ sudo pip install PyBeacon --upgrade
+
+## Usage
+ PyBeacon [-h] [-u [URL]] [-s] [-t] [-o] [-v] [-V]
+
+ optional arguments:
+ -h, --help show this help message and exit
+ -u [URL], --url [URL] URL to advertise.
+ -s, --scan Scan for URLs.
+ -t, --terminate Stop advertising URL.
+ -o, --one Scan one URL only.
+ -v, --version Version of PyBeacon.
+ -V, --verbose Print lots of debug output.
\ No newline at end of file
diff --git a/eddystone-url/implementations/PyBeacon/setup.cfg b/eddystone-url/implementations/PyBeacon/setup.cfg
new file mode 100644
index 0000000..aa35ccb
--- /dev/null
+++ b/eddystone-url/implementations/PyBeacon/setup.cfg
@@ -0,0 +1,3 @@
+# PyBeacon
+[metadata]
+description-file = README.md
\ No newline at end of file
diff --git a/eddystone-url/implementations/PyBeacon/setup.py b/eddystone-url/implementations/PyBeacon/setup.py
new file mode 100644
index 0000000..2e072f8
--- /dev/null
+++ b/eddystone-url/implementations/PyBeacon/setup.py
@@ -0,0 +1,36 @@
+from setuptools import setup
+
+setup(
+ name='PyBeacon',
+ version='0.2.4.3',
+ packages=['PyBeacon'],
+ entry_points = {
+ "console_scripts": ['PyBeacon = PyBeacon.PyBeacon:main']
+ },
+
+ description ='Python script for scanning and advertising urls over Eddystone-URL.',
+
+ url='https://github.com/nirmankarta/PyBeacon',
+
+ author='Nirmankarta',
+
+ license='MIT',
+
+ classifiers=[
+
+ 'Development Status :: 4 - Beta',
+
+ 'Intended Audience :: Developers',
+ 'Topic :: Software Development :: Build Tools',
+
+ 'License :: OSI Approved :: MIT License',
+
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.6',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ ],
+)
\ No newline at end of file
diff --git a/eddystone-url/implementations/README.md b/eddystone-url/implementations/README.md
index 8fc3634..14aa9bf 100644
--- a/eddystone-url/implementations/README.md
+++ b/eddystone-url/implementations/README.md
@@ -9,10 +9,13 @@ under the licenses described in each subdirectory. These projects serve as a gui
write you own Eddystone-URL beacon code. We encourage you to add additional examples.
To date, in this directory you will find the following implementations:
+* [Arduino (BLEPeripheral)](https://github.com/sandeepmistry/arduino-BLEPeripheral/blob/master/examples/Eddystone/EddystoneURL/EddystoneURL.ino), a list of compatible hardware can be found [here](https://github.com/sandeepmistry/arduino-BLEPeripheral#compatible-hardware).
+* [BBC micro:bit](https://github.com/showio/microbit-physicalweb)
* [BlueGiga BLED112](BlueGiga-BLED112)
* [Cambridge Silicon Radio CSR1010 (Beacon Development Board)](CSR-1010)
-* [RFduino](RFduino)
-* [Linux (bluez)](linux-url-advertiser)
+* [PyBeacon (Python Package)](PyBeacon), for latest builds, check [Nirmanakarta/PyBeacon](https://github.com/nirmankarta/PyBeacon)
+* [Linux (bluez)](linux)
* [ARM mbed (Nordic nRF51-dongle, nRF51-DK))](mbed_EddystoneURL_Beacon)
* [Node.js (node-eddystone-beacon)](https://github.com/don/node-eddystone-beacon)
-* [Arduino (BLEPeripheral)](https://github.com/sandeepmistry/arduino-BLEPeripheral/blob/master/examples/Eddystone/EddystoneURL/EddystoneURL.ino), a list of compatible hardware can be found [here](https://github.com/sandeepmistry/arduino-BLEPeripheral#compatible-hardware).
+* [RFduino](RFduino)
+* [TI-CC2640](TI-CC2640)
\ No newline at end of file
diff --git a/eddystone-url/implementations/linux-url-advertiser/README.md b/eddystone-url/implementations/linux-url-advertiser/README.md
deleted file mode 100644
index 0b1c8a3..0000000
--- a/eddystone-url/implementations/linux-url-advertiser/README.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Url advertiser for linux
-
-A python script that advertises a url using Eddystone-URL
-
-## Installation
-
- sudo apt-get install bluez
-
-## Usage
-
- # Advertising a url
- ./advertise-url http://your/url
-
- # Stopping the advertisement
- ./advertise-url -s
diff --git a/eddystone-url/implementations/linux-url-advertiser/LICENSE b/eddystone-url/implementations/linux/LICENSE
similarity index 100%
rename from eddystone-url/implementations/linux-url-advertiser/LICENSE
rename to eddystone-url/implementations/linux/LICENSE
diff --git a/eddystone-url/implementations/linux/README.md b/eddystone-url/implementations/linux/README.md
new file mode 100644
index 0000000..bddbc9c
--- /dev/null
+++ b/eddystone-url/implementations/linux/README.md
@@ -0,0 +1,30 @@
+# Url advertiser for linux
+
+A set of python scripts for scanning and advertising urls over Eddystone-URL.
+
+## Requirements
+
+ Linux
+ Python 3
+ bluez
+
+## Installation
+
+ sudo apt-get install bluez
+
+## Usage
+
+ # Advertising a url
+ ./advertise-url -u http://your/url
+
+ # Stopping the advertisement
+ ./advertise-url -s
+
+ # Running a single scan for urls
+ ./scan-for-urls -s
+
+ # A continuous scan for urls
+ ./scan-for-urls
+
+ # A continuous scan and resolving short urls to long urls
+ ./scan-for-urls | ./resolve-urls -u [URLs]
diff --git a/eddystone-url/implementations/linux-url-advertiser/advertise-url b/eddystone-url/implementations/linux/advertise-url
similarity index 55%
rename from eddystone-url/implementations/linux-url-advertiser/advertise-url
rename to eddystone-url/implementations/linux/advertise-url
index 8969659..5b0bd2b 100755
--- a/eddystone-url/implementations/linux-url-advertiser/advertise-url
+++ b/eddystone-url/implementations/linux/advertise-url
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
#
# Copyright 2015 Opera Software ASA. All rights reserved.
#
@@ -13,28 +13,34 @@
# 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.
-
+"""
+Advertises a url as an Eddystone beacon.
+"""
+import os
import sys
import subprocess
-from optparse import OptionParser
+import argparse
-parser = OptionParser(usage="%prog [options] [url]")
+if (sys.version_info > (3, 0)):
+ DEVNULL = subprocess.DEVNULL
+else:
+ DEVNULL = open(os.devnull, 'wb')
+# The default url
+url = "https://goo.gl/SkcDTN"
-parser.add_option("-s", "--stop", dest="stop",
- action="store_true", default=False,
- help="Stop advertising")
+parser = argparse.ArgumentParser(prog='advertise-url', description= __doc__)
+parser.add_argument("-u", "--url", nargs='?', const=url, type=str,
+ default=url, help='URL to advertise.')
-parser.add_option("-v", "--verbose", dest="verbose",
- action="store_true", default=False,
- help="Print lots of debug output")
+parser.add_argument('-s','--stop', action='store_true',
+ help='Stop advertising url.')
-(options, args) = parser.parse_args()
+parser.add_argument("-v", "--verbose", action='store_true',
+ help='Print lots of debug output.')
-# The default uri
-uri = "https://goo.gl/SkcDTN"
+options = parser.parse_args()
-if len(args) > 0:
- uri = args[0]
+url = options.url
schemes = [
"http://www.",
@@ -43,33 +49,35 @@ schemes = [
"https://",
]
-expansions = [
+extensions = [
".com/", ".org/", ".edu/", ".net/", ".info/", ".biz/", ".gov/",
".com", ".org", ".edu", ".net", ".info", ".biz", ".gov",
]
-def verboseOutput(text):
+
+def verboseOutput(text = ""):
if options.verbose:
sys.stderr.write(text + "\n")
-def encodeUri(uri):
+
+def encodeurl(url):
i = 0
data = []
for s in range(len(schemes)):
scheme = schemes[s]
- if uri.startswith(scheme):
+ if url.startswith(scheme):
data.append(s)
i += len(scheme)
- break;
+ break
else:
- raise Exception("Invalid uri scheme")
+ raise Exception("Invalid url scheme")
- while i < len(uri):
- if uri[i] == '.':
- for e in range(len(expansions)):
- expansion = expansions[e]
- if uri.startswith(expansion, i):
+ while i < len(url):
+ if url[i] == '.':
+ for e in range(len(extensions)):
+ expansion = extensions[e]
+ if url.startswith(expansion, i):
data.append(e)
i += len(expansion)
break
@@ -77,18 +85,19 @@ def encodeUri(uri):
data.append(0x2E)
i += 1
else:
- data.append(ord(uri[i]))
+ data.append(ord(url[i]))
i += 1
return data
-def encodeMessage(uri):
- encodedUri = encodeUri(uri)
- encodedUriLength = len(encodedUri)
- verboseOutput("Encoded uri length: " + str(encodedUriLength))
+def encodeMessage(url):
+ encodedurl = encodeurl(url)
+ encodedurlLength = len(encodedurl)
+
+ verboseOutput("Encoded url length: " + str(encodedurlLength))
- if encodedUriLength > 18:
+ if encodedurlLength > 18:
raise Exception("Encoded url too long (max 18 bytes)")
message = [
@@ -101,30 +110,24 @@ def encodeMessage(uri):
0xaa, # 16-bit Eddystone UUID
0xfe, # 16-bit Eddystone UUID
- 5 + len(encodedUri), # Service Data length
+ 5 + len(encodedurl), # Service Data length
0x16, # Service Data data type value
0xaa, # 16-bit Eddystone UUID
0xfe, # 16-bit Eddystone UUID
- 0x10, # Eddystone-URL frame type
+ 0x10, # Eddystone-url frame type
0xed, # txpower
]
- message += encodedUri
+ message += encodedurl
return message
-def systemCall(command):
- verboseOutput(command)
- child = subprocess.Popen(["-c", command],
- stdout = subprocess.PIPE,
- stderr = subprocess.PIPE,
- shell = True)
- child.communicate()
-def advertise(uri):
- verboseOutput("Advertising: " + uri)
- message = encodeMessage(uri)
+def advertise(url):
+ print("Advertising: " + url)
+ verboseOutput("Advertising: " + url)
+ message = encodeMessage(url)
# Prepend the length of the whole message
message.insert(0, len(message))
@@ -139,25 +142,29 @@ def advertise(uri):
message = " ".join(message)
verboseOutput("Message: " + message)
- systemCall("sudo hciconfig hci0 up")
+ subprocess.call("sudo hciconfig hci0 up", shell = True, stdout = DEVNULL)
+
# Stop advertising
- systemCall("sudo hcitool -i hci0 cmd 0x08 0x000a 00")
+ subprocess.call("sudo hcitool -i hci0 cmd 0x08 0x000a 00", shell = True, stdout = DEVNULL)
# Set message
- systemCall("sudo hcitool -i hci0 cmd 0x08 0x0008 " + message)
+ subprocess.call("sudo hcitool -i hci0 cmd 0x08 0x0008 " + message, shell = True, stdout = DEVNULL)
# Resume advertising
- systemCall("sudo hcitool -i hci0 cmd 0x08 0x000a 01")
+ subprocess.call("sudo hcitool -i hci0 cmd 0x08 0x000a 01", shell = True, stdout = DEVNULL)
+
def stopAdvertising():
+ print("Stopping advertising")
verboseOutput("Stopping advertising")
- systemCall("sudo hcitool -i hci0 cmd 0x08 0x000a 00")
+ subprocess.call("sudo hcitool -i hci0 cmd 0x08 0x000a 00", shell = True, stdout = DEVNULL)
+
try:
if options.stop:
stopAdvertising()
else:
- advertise(uri)
+ advertise(url)
except Exception as e:
sys.stderr.write("Exception: " + str(e) + "\n")
exit(1)
diff --git a/eddystone-url/implementations/linux/resolve-urls b/eddystone-url/implementations/linux/resolve-urls
new file mode 100755
index 0000000..c4b2add
--- /dev/null
+++ b/eddystone-url/implementations/linux/resolve-urls
@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+#
+# Copyright 2015 Opera Software ASA. All rights reserved.
+#
+# 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.
+"""
+Resolve url redirects and output the final urls. Input can be given either on
+the command line, or through standard input. Try: "./scan-for-urls |
+./resolve-urls"
+"""
+
+import http.client
+import sys
+import urllib.parse
+import argparse
+
+parser = argparse.ArgumentParser(prog='advertise-url', description= __doc__)
+parser.add_argument("-u", "--urls", nargs='+', type=str, required=True,
+ help='URLs to resolve.')
+
+args = parser.parse_args()
+
+
+def resolveUrl(url):
+ """
+ Follows redirects until the final url is found.
+ """
+
+ try:
+ parsed = urllib.parse.urlsplit(url)
+
+ conn = None
+ if parsed.scheme == "https":
+ conn = http.client.HTTPSConnection(parsed.netloc)
+ elif parsed.scheme == "http":
+ conn = http.client.HTTPConnection(parsed.netloc)
+
+ path = parsed.path
+ if parsed.query:
+ path += "&" + parsed.query
+
+ conn.request("HEAD", path)
+
+ response = conn.getresponse()
+
+ if response.status >= 300 and response.status < 400:
+ return resolveUrl(response.getheader("Location"))
+ else:
+ return url
+
+ except:
+ return url
+
+
+resolvedUrls = dict()
+
+for url in args.urls or sys.stdin:
+ url = url.strip()
+
+ if not url in resolvedUrls:
+ resolvedUrls[url] = resolveUrl(url)
+
+ print(resolvedUrls[url])
diff --git a/eddystone-url/implementations/linux/scan-for-urls b/eddystone-url/implementations/linux/scan-for-urls
new file mode 100755
index 0000000..1def934
--- /dev/null
+++ b/eddystone-url/implementations/linux/scan-for-urls
@@ -0,0 +1,173 @@
+#!/usr/bin/env python3
+#
+# Copyright 2015 Opera Software ASA. All rights reserved.
+#
+# 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.
+"""
+Scan for URLs from Eddystone and UriBeacon bluetooth beacons.
+"""
+
+import re
+import signal
+import subprocess
+import sys
+import time
+
+import argparse
+
+parser = argparse.ArgumentParser(prog='scan-for-urls', description= __doc__)
+
+parser.add_argument('-s','--single', action='store_true',
+ help='Perform a single scan and output all the urls found without duplicates.')
+
+parser.add_argument("-v", "--verbose", action='store_true',
+ help='Print lots of debug output.')
+
+options = parser.parse_args()
+
+schemes = [
+ "http://www.",
+ "https://www.",
+ "http://",
+ "https://",
+ ]
+
+extensions = [
+ ".com/", ".org/", ".edu/", ".net/", ".info/", ".biz/", ".gov/",
+ ".com", ".org", ".edu", ".net", ".info", ".biz", ".gov",
+ ]
+
+
+def verboseOutput(text = ""):
+ if options.verbose:
+ sys.stderr.write(text + "\n")
+
+
+def decodeUrl(encodedUrl):
+ """
+ Decode a url encoded with the Eddystone (or UriBeacon) URL encoding scheme
+ """
+
+ decodedUrl = schemes[encodedUrl[0]]
+ for c in encodedUrl[1:]:
+ if c <= 0x20:
+ decodedUrl += extensions[c]
+ else:
+ decodedUrl += chr(c)
+
+ return decodedUrl
+
+
+def onUrlFound(url):
+ """
+ Called by onPacketFound, if the packet contains a url.
+ """
+
+ sys.stdout.write(url)
+ sys.stdout.write("\n")
+ sys.stdout.flush()
+
+
+foundPackets = set()
+
+def onPacketFound(packet):
+ """
+ Called by the scan function for each beacon packets found.
+ """
+
+ data = bytearray.fromhex(packet)
+
+ if options.single:
+ tmp = packet[:-3]
+ if tmp in foundPackets:
+ return
+ foundPackets.add(tmp)
+
+ # Eddystone
+ if len(data) >= 20 and data[19] == 0xaa and data[20] == 0xfe:
+ serviceDataLength = data[21]
+ frameType = data[25]
+
+ # Eddystone-URL
+ if frameType == 0x10:
+ verboseOutput("Eddystone-URL")
+ onUrlFound(decodeUrl(data[27:22 + serviceDataLength]))
+ elif frameType == 0x00:
+ verboseOutput("Eddystone-UID")
+ elif frameType == 0x20:
+ verboseOutput("Eddystone-TLM")
+ else:
+ verboseOutput("Unknown Eddystone frame type: {}".format(frameType))
+
+ # UriBeacon
+ elif len(data) >= 20 and data[19] == 0xd8 and data[20] == 0xfe:
+ serviceDataLength = data[21]
+ verboseOutput("UriBeacon")
+ onUrlFound(decodeUrl(data[27:22 + serviceDataLength]))
+
+ else:
+ verboseOutput("Unknown beacon type")
+
+ verboseOutput(packet)
+ verboseOutput()
+
+
+def scan(duration = None):
+ """
+ Scan for beacons. This function scans for [duration] seconds. If duration
+ is set to None, it scans until interrupted.
+ """
+
+ subprocess.call(["sudo", "-v"])
+ subprocess.call("sudo hciconfig hci0 reset", shell = True, stdout = subprocess.DEVNULL)
+
+ lescan = subprocess.Popen(
+ ["sudo", "-n", "hcitool", "lescan", "--duplicates"],
+ stdout = subprocess.DEVNULL)
+
+ dump = subprocess.Popen(
+ ["sudo", "-n", "hcidump", "--raw"],
+ stdout = subprocess.PIPE)
+
+ packet = None
+ try:
+ startTime = time.time()
+ for line in dump.stdout:
+ line = line.decode()
+ if line.startswith("> "):
+ if packet: onPacketFound(packet)
+ packet = line[2:].strip()
+ elif line.startswith("< "):
+ if packet: onPacketFound(packet)
+ packet = None
+ else:
+ if packet: packet += " " + line.strip()
+
+ if duration and time.time() - startTime > duration:
+ break
+
+ except KeyboardInterrupt:
+ pass
+
+ subprocess.call(["sudo", "kill", str(dump.pid), "-s", "SIGINT"])
+ subprocess.call(["sudo", "-n", "kill", str(lescan.pid), "-s", "SIGINT"])
+
+
+try:
+ if options.single:
+ scan(3)
+ else:
+ scan()
+except Exception as e:
+ sys.stderr.write("Exception: " + str(e) + "\n")
+ exit(1)
diff --git a/eddystone-url/implementations/nodejs/README.md b/eddystone-url/implementations/nodejs/README.md
new file mode 100644
index 0000000..ec59662
--- /dev/null
+++ b/eddystone-url/implementations/nodejs/README.md
@@ -0,0 +1,3 @@
+# Node.js
+
+[node-eddystone-beacon](https://github.com/don/node-eddystone-beacon)
\ No newline at end of file
diff --git a/implementations/README.md b/implementations/README.md
new file mode 100644
index 0000000..003ffa8
--- /dev/null
+++ b/implementations/README.md
@@ -0,0 +1,12 @@
+# Eddystone Implementations
+There are two goals of this list:
+
+* Collect a list of Eddystone beacon open source implementations that fully support the [Eddystone protocol](https://github.com/google/eddystone/blob/master/protocol-specification.md) and the [GATT configuration service](https://github.com/google/eddystone/blob/master/configuration-service)
+* Encourage embedding Eddystone functionality into other products (usually by adding additional GATT services to the beacon)
+
+Any forks of these projects that add functionality should also be listed here as long as they too are forkable
+
+
+* [ARM mbed](mbed)
+* [Nordic](https://github.com/NordicSemiconductor/nrf5-sdk-for-eddystone)
+
diff --git a/implementations/mbed/.gitignore b/implementations/mbed/.gitignore
new file mode 100644
index 0000000..7c9d80f
--- /dev/null
+++ b/implementations/mbed/.gitignore
@@ -0,0 +1,34 @@
+ # Compiled Object files
+ *.slo
+ *.lo
+ *.o
+ *.obj
+
+ # Precompiled Headers
+ *.gch
+ *.pch
+
+ # Compiled Dynamic libraries
+ *.so
+ *.dylib
+ *.dll
+
+ # Fortran module files
+ *.mod
+
+ # Compiled Static libraries
+ *.lai
+ *.la
+ *.a
+ -*.lib
+
+ # Executables
+ *.exe
+ *.out
+ *.app
+
+ .yotta.json
+ build/
+ yotta_modules/
+ yotta_targets/
+ .DS_Store
diff --git a/implementations/mbed/.mbedignore b/implementations/mbed/.mbedignore
new file mode 100644
index 0000000..06aa14c
--- /dev/null
+++ b/implementations/mbed/.mbedignore
@@ -0,0 +1,9 @@
+mbed-os/rtos/*
+mbed-os/features/FEATURE_CLIENT/*
+mbed-os/features/FEATURE_COMMON_PAL/*
+mbed-os/features/FEATURE_UVISOR/*
+mbed-os/features/frameworks/*
+mbed-os/features/net/*
+mbed-os/features/netsocket/*
+mbed-os/features/storage/*
+mbed-os/events/*
diff --git a/implementations/mbed/LICENSE b/implementations/mbed/LICENSE
new file mode 100644
index 0000000..8dada3e
--- /dev/null
+++ b/implementations/mbed/LICENSE
@@ -0,0 +1,201 @@
+ 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
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ 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.
diff --git a/implementations/mbed/README.md b/implementations/mbed/README.md
new file mode 100644
index 0000000..99a2c14
--- /dev/null
+++ b/implementations/mbed/README.md
@@ -0,0 +1,24 @@
+# Eddystone for mbed
+Eddystone beacons broadcast a small amount of information, such as URLs, to nearby devices that scan for them. If you'd like to learn more, please see the [Physical Web](http://physical-web.org) page.
+
+This repo contains an fully functional Eddystone image that supports all 4 frame types (URL, UUID, TLM, and EID) as well as the complete Eddystone GATT configuration service.
+
+To compile this image you'll need the [mbed toolchain from ARM](mbed.org). mbed is one of the most widely used embedded OS platforms. Unfortunately, we can't offer support for mbed or ARM tools.
+
+### Goal 1 - Lots of beacons
+The first goal of this repo is to encourage a wide distribution of Eddystone beacon hardware with an open source version that anyone can freely use. If you do port this to your platform, please consider a pull request so others can compile to your hardware. There is already one comercial beacon using this image mde by [MinewTech](http://www.minewtech.com/eddystone.html) (we hope many more will follow)
+
+### Goal 2 - Lots of devices
+The second goal is to encourage other devices beyond just beacons. These would be interative devices that use new GATT services characteristics. We hope to see forks for products such as vending machines or remote control toys. With Bluetooth Javascript support in modern web browsers, it's important we have an easy way for device makers to not only broadcast a URL but to connect and control it directly.
+
+### Porting the code
+1. Edit Eddystone_config.h, most boards should work with changes only to this file.
+2. There are #defines for each target board, just add your own #define to the list
+
+**Note:** We've only compiled this for the Nordic chipsets as of Jan 2017. If you are using anything else, there will almost certainly be other changes to make throughout the code (such as persistent storage). We are encouraging new chip vendors to make these changes so you don't have to.
+
+### Best Practices
+* Be sure to test the advertised transmit power levels as each board's antenna is different.
+* The URL for configuring the beacon, cf.physical-web.org is free for anyone to use. You don't need to change it.
+* Offer a wide range of power levels so it's possible to broadcast only a short distance.
+* Please don't default to high power. We don't want 'shouty' beacons
diff --git a/implementations/mbed/config.json b/implementations/mbed/config.json
new file mode 100644
index 0000000..814e0bf
--- /dev/null
+++ b/implementations/mbed/config.json
@@ -0,0 +1,20 @@
+{
+ "nordic": {
+ "softdevice": "S130"
+ },
+ "nrf51822": {
+ "ram_size": "32K"
+ },
+ "mbed": {
+ "max-filehandles": 4
+ },
+ "platform" : {
+ "reset_button" : "p17",
+ "config_led" : "p13",
+ "shutdown_led" : "p14",
+ "led_off" : 0
+ },
+ "mbedtls": {
+ "user-config-file": "\"source/mbedtls_config.h\""
+ }
+}
diff --git a/implementations/mbed/img/app_start.png b/implementations/mbed/img/app_start.png
new file mode 100644
index 0000000..9bb78b3
Binary files /dev/null and b/implementations/mbed/img/app_start.png differ
diff --git a/implementations/mbed/img/edit_url.png b/implementations/mbed/img/edit_url.png
new file mode 100644
index 0000000..f0a048d
Binary files /dev/null and b/implementations/mbed/img/edit_url.png differ
diff --git a/implementations/mbed/img/open_configuration.png b/implementations/mbed/img/open_configuration.png
new file mode 100644
index 0000000..678a582
Binary files /dev/null and b/implementations/mbed/img/open_configuration.png differ
diff --git a/implementations/mbed/img/result.png b/implementations/mbed/img/result.png
new file mode 100644
index 0000000..68d3cc2
Binary files /dev/null and b/implementations/mbed/img/result.png differ
diff --git a/implementations/mbed/img/save_url.png b/implementations/mbed/img/save_url.png
new file mode 100644
index 0000000..6422828
Binary files /dev/null and b/implementations/mbed/img/save_url.png differ
diff --git a/implementations/mbed/mbed-os.lib b/implementations/mbed/mbed-os.lib
new file mode 100644
index 0000000..34be6c1
--- /dev/null
+++ b/implementations/mbed/mbed-os.lib
@@ -0,0 +1 @@
+https://github.com/ARMmbed/mbed-os/#aeabcc947294f2a6cbe1e808c030779c7c460f10
diff --git a/implementations/mbed/mbed_app.json b/implementations/mbed/mbed_app.json
new file mode 100644
index 0000000..db9db7e
--- /dev/null
+++ b/implementations/mbed/mbed_app.json
@@ -0,0 +1,12 @@
+{
+ "macros": [
+ "NDEBUG=1",
+ "MBEDTLS_USER_CONFIG_FILE=\"mbedtls_config.h\"",
+ "OS_MAINSTKSIZE=1024"
+ ],
+ "target_overrides": {
+ "*": {
+ "platform.stdio-flush-at-exit": false
+ }
+ }
+}
diff --git a/implementations/mbed/source/EIDFrame.cpp b/implementations/mbed/source/EIDFrame.cpp
new file mode 100644
index 0000000..761cb91
--- /dev/null
+++ b/implementations/mbed/source/EIDFrame.cpp
@@ -0,0 +1,206 @@
+/*
+ * Copyright (c) 2016, Google Inc, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+
+#include "EIDFrame.h"
+#include "EddystoneService.h"
+#include "EntropySource/EntropySource.h"
+
+EIDFrame::EIDFrame()
+{
+ mbedtls_entropy_init(&entropy);
+ // init entropy source
+ eddystoneRegisterEntropySource(&entropy);
+ // init Random
+ mbedtls_ctr_drbg_init(&ctr_drbg);
+}
+
+void EIDFrame::clearFrame(uint8_t* frame) {
+ frame[FRAME_LEN_OFFSET] = 0; // Set frame length to zero to clear it
+}
+
+
+void EIDFrame::setData(uint8_t *rawFrame, int8_t advTxPower, const uint8_t* eidData)
+{
+ size_t index = 0;
+ rawFrame[index++] = EDDYSTONE_UUID_SIZE + EID_FRAME_LEN; // EID length + overhead of four bytes below
+ rawFrame[index++] = EDDYSTONE_UUID[0]; // 16-bit Eddystone UUID
+ rawFrame[index++] = EDDYSTONE_UUID[1];
+ rawFrame[index++] = FRAME_TYPE_EID; // 1B Type
+ rawFrame[index++] = advTxPower; // 1B Power @ 0meter
+
+ memcpy(rawFrame + index, eidData, EID_LENGTH); // EID = 8 BYTE ID
+}
+
+uint8_t* EIDFrame::getData(uint8_t* rawFrame)
+{
+ return &(rawFrame[EID_DATA_OFFSET]);
+}
+
+uint8_t EIDFrame::getDataLength(uint8_t* rawFrame)
+{
+ return rawFrame[FRAME_LEN_OFFSET] - EDDYSTONE_UUID_LEN;
+}
+
+uint8_t* EIDFrame::getAdvFrame(uint8_t* rawFrame)
+{
+ return &(rawFrame[ADV_FRAME_OFFSET]);
+}
+
+uint8_t EIDFrame::getAdvFrameLength(uint8_t* rawFrame)
+{
+ return rawFrame[FRAME_LEN_OFFSET];
+}
+
+uint8_t* EIDFrame::getEid(uint8_t* rawFrame)
+{
+ return &(rawFrame[EID_VALUE_OFFSET]);
+}
+
+uint8_t EIDFrame::getEidLength(uint8_t* rawFrame)
+{
+ return rawFrame[FRAME_LEN_OFFSET] - EID_HEADER_LEN;
+}
+
+void EIDFrame::setAdvTxPower(uint8_t* rawFrame, int8_t advTxPower)
+{
+ rawFrame[EID_TXPOWER_OFFSET] = advTxPower;
+}
+
+// Mote: This is only called after the rotation period is due, or on writing/creating a new eidIdentityKey
+void EIDFrame::update(uint8_t* rawFrame, uint8_t* eidIdentityKey, uint8_t rotationPeriodExp, uint32_t timeSecs)
+{
+ // Calculate the temporary key datastructure 1
+ uint8_t ts[4]; // big endian representation of time
+ ts[0] = (timeSecs >> 24) & 0xff;
+ ts[1] = (timeSecs >> 16) & 0xff;
+
+ uint8_t tmpEidDS1[16] = { 0,0,0,0,0,0,0,0,0,0,0, SALT, 0, 0, ts[0], ts[1] };
+
+ // Perform the aes encryption to generate the final temporary key.
+ uint8_t tmpKey[16];
+ aes128Encrypt(eidIdentityKey, tmpEidDS1, tmpKey);
+
+ // Compute the EID
+ uint8_t eid[16];
+ uint32_t scaledTime = (timeSecs >> rotationPeriodExp) << rotationPeriodExp;
+ ts[0] = (scaledTime >> 24) & 0xff;
+ ts[1] = (scaledTime >> 16) & 0xff;
+ ts[2] = (scaledTime >> 8) & 0xff;
+ ts[3] = scaledTime & 0xff;
+ uint8_t tmpEidDS2[16] = { 0,0,0,0,0,0,0,0,0,0,0, rotationPeriodExp, ts[0], ts[1], ts[2], ts[3] };
+ aes128Encrypt(tmpKey, tmpEidDS2, eid);
+
+ // copy the leading 8 bytes of the eid result (full result length = 16) into the ADV frame
+ memcpy(rawFrame + 5, eid, EID_LENGTH);
+
+}
+
+/** AES128 encrypts a 16-byte input array with a key, resulting in a 16-byte output array */
+void EIDFrame::aes128Encrypt(uint8_t key[], uint8_t input[], uint8_t output[]) {
+ mbedtls_aes_context ctx;
+ mbedtls_aes_init(&ctx);
+ mbedtls_aes_setkey_enc(&ctx, key, 8 * sizeof(Lock_t));
+ mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, input, output);
+ mbedtls_aes_free(&ctx);
+}
+
+int EIDFrame::genBeaconKeys(PrivateEcdhKey_t beaconPrivateEcdhKey, PublicEcdhKey_t beaconPublicEcdhKey) {
+ mbedtls_ecdh_init( &ecdh_ctx );
+
+ int i = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, NULL, 0);
+ if (i != 0) {
+ return i; // return EID_RND_FAIL;
+ }
+
+ if (mbedtls_ecp_group_load(&ecdh_ctx.grp, MBEDTLS_ECP_DP_CURVE25519) != 0) {
+ return EID_GRP_FAIL;
+ }
+ if (mbedtls_ecdh_gen_public(&ecdh_ctx.grp, &ecdh_ctx.d, &ecdh_ctx.Q, mbedtls_ctr_drbg_random, &ctr_drbg) != 0) {
+ return EID_GENKEY_FAIL;
+ }
+
+ mbedtls_mpi_write_binary(&ecdh_ctx.d, beaconPrivateEcdhKey, sizeof(PrivateEcdhKey_t));
+ mbedtls_mpi_write_binary(&ecdh_ctx.Q.X, beaconPublicEcdhKey, sizeof(PublicEcdhKey_t));
+
+ mbedtls_ecdh_free( &ecdh_ctx );
+ return EID_SUCCESS;
+}
+
+int EIDFrame::genEcdhSharedKey(PrivateEcdhKey_t beaconPrivateEcdhKey, PublicEcdhKey_t beaconPublicEcdhKey, PublicEcdhKey_t serverPublicEcdhKey, EidIdentityKey_t eidIdentityKey) {
+ int16_t ret = 0;
+ uint8_t tmp[32];
+ // initialize context
+ mbedtls_ecdh_init( &ecdh_ctx );
+ mbedtls_ecp_group_load( &ecdh_ctx.grp, MBEDTLS_ECP_DP_CURVE25519 );
+
+ // copy binary beacon private key (previously generated!) into context
+ // Note: As the PrivateKey is generated locally, it is Big Endian
+ ret = mbedtls_mpi_read_binary( &ecdh_ctx.d, beaconPrivateEcdhKey, sizeof(PrivateEcdhKey_t) );
+
+ // copy server-public-key (received through GATT characteristic 10) into context
+ ret = mbedtls_mpi_lset( &ecdh_ctx.Qp.Z, 1 );
+ EddystoneService::swapEndianArray(serverPublicEcdhKey, tmp, 32); // To make it Big Endian
+ ret = mbedtls_mpi_read_binary( &ecdh_ctx.Qp.X, tmp , sizeof(PublicEcdhKey_t) );
+
+ // ECDH point multiplication
+ size_t olen; // actual size of shared secret
+ uint8_t sharedSecret[32]; // shared ECDH secret
+ memset(sharedSecret, 0, 32);
+ ret = mbedtls_ecdh_calc_secret( &ecdh_ctx, &olen, sharedSecret, sizeof(sharedSecret), NULL, NULL );
+ LOG(("size of olen= %d ret=%x\r\n", olen, ret));
+ EddystoneService::swapEndianArray(sharedSecret, tmp, 32);
+ memcpy(sharedSecret, tmp, 32);
+ LOG(("Shared secret=")); EddystoneService::logPrintHex(sharedSecret, 32);
+ if (olen != sizeof(sharedSecret)) {
+ return EID_GENKEY_FAIL;
+ }
+ if (ret == MBEDTLS_ERR_ECP_BAD_INPUT_DATA) {
+ return EID_RC_SS_IS_ZERO;
+ }
+
+ // Convert the shared secret to key material using HKDF-SHA256. HKDF is used with
+ // the salt set to a concatenation of the resolver's public key and beacon's
+ // public key, with a null context.
+
+ // build HKDF key
+ unsigned char k[ 64 ];
+ EddystoneService::swapEndianArray(beaconPublicEcdhKey, tmp, 32);
+ memcpy( &k[0], serverPublicEcdhKey, sizeof(PublicEcdhKey_t) );
+ memcpy( &k[32], tmp, sizeof(PublicEcdhKey_t) );
+
+ // compute HKDF: see https://tools.ietf.org/html/rfc5869
+ // mbedtls_md_context_t md_ctx;
+ mbedtls_md_init( &md_ctx );
+ mbedtls_md_setup( &md_ctx, mbedtls_md_info_from_type( MBEDTLS_MD_SHA256 ), 1 );
+ mbedtls_md_hmac_starts( &md_ctx, k, sizeof( k ) );
+ mbedtls_md_hmac_update( &md_ctx, sharedSecret, sizeof(sharedSecret) );
+ unsigned char prk[ 32 ];
+ mbedtls_md_hmac_finish( &md_ctx, prk );
+ mbedtls_md_hmac_starts( &md_ctx, prk, sizeof( prk ) );
+ const unsigned char const1[] = { 0x01 };
+ mbedtls_md_hmac_update( &md_ctx, const1, sizeof( const1 ) );
+ unsigned char t[ 32 ];
+ mbedtls_md_hmac_finish( &md_ctx, t );
+
+ //Truncate the key material to 16 bytes (128 bits) to convert it to an AES-128 secret key.
+ memcpy( eidIdentityKey, t, sizeof(EidIdentityKey_t) );
+ LOG(("\r\nEIDIdentityKey=")); EddystoneService::logPrintHex(t, 32); LOG(("\r\n"));
+
+ mbedtls_md_free( &md_ctx );
+ mbedtls_ecdh_free( &ecdh_ctx );
+ return EID_SUCCESS;
+}
diff --git a/implementations/mbed/source/EIDFrame.h b/implementations/mbed/source/EIDFrame.h
new file mode 100644
index 0000000..b936b06
--- /dev/null
+++ b/implementations/mbed/source/EIDFrame.h
@@ -0,0 +1,224 @@
+/*
+ * Copyright (c) 2016, Google Inc, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+
+#ifndef __EIDFRAME_H__
+#define __EIDFRAME_H__
+
+#include
+#include "EddystoneTypes.h"
+#include "mbedtls/aes.h"
+#include "mbedtls/ecdh.h"
+#include "mbedtls/md.h"
+#include "mbedtls/entropy.h"
+#include "mbedtls/ctr_drbg.h"
+#include "aes_eax.h"
+
+/**
+ * Class that encapsulates data that belongs to the Eddystone-EID frame. For
+ * more information refer to https://github.com/google/eddystone/tree/master/eddystone-EID.
+ */
+class EIDFrame
+{
+public:
+ static const uint8_t SALT = 0xff;
+ static const uint8_t EID_LENGTH = 8;
+ static const int EID_SUCCESS = 0;
+ static const int EID_RC_SS_IS_ZERO = -1;
+ static const int EID_RND_FAIL = -2;
+ static const int EID_GRP_FAIL = -3;
+ static const int EID_GENKEY_FAIL = -4;
+
+ /**
+ * Construct a new instance of this class.
+ */
+ EIDFrame();
+
+ /**
+ * Clear frame (internally represented by length = 0 )
+ */
+ void clearFrame(uint8_t* frame);
+
+ /**
+ * Construct the raw bytes of the Eddystone-EID frame that will be directly
+ * used in the advertising packets.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ * @param[in] advPowerLevel
+ * Power level value included in the raw frame.
+ * @param[in] eidData
+ * The actual 16-byte EID data in the raw frame.
+ */
+ void setData(uint8_t* rawFrame, int8_t advTxPower, const uint8_t* eidData);
+
+ /**
+ * Get the EID frame data from the Eddystone-EID frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return A pointer to the bytes of the Eddystone-EID frame data.
+ */
+ uint8_t* getData(uint8_t* rawFrame);
+
+ /**
+ * Get the length of the EID frame data from the Eddystone-EID frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return The size in bytes of the Eddystone-EID frame.
+ */
+ uint8_t getDataLength(uint8_t* rawFrame);
+
+ /**
+ * Get the EID Adv data from the Eddystone-EID frame.
+ * This is the full service data included in the BLE service data params
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return A pointer to the bytes of the Eddystone-EID Adv frame data.
+ */
+ uint8_t* getAdvFrame(uint8_t* rawFrame);
+
+ /**
+ * Get the length of the EID Adv data from the Eddystone-EID frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return The size in bytes of the Eddystone-EID Adv frame data.
+ */
+ uint8_t getAdvFrameLength(uint8_t* rawFrame);
+
+ /**
+ * Get just the EID data from the Eddystone-EID frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return A pointer to the bytes of the EID in the Eddystone-EID frame.
+ */
+ uint8_t* getEid(uint8_t* rawFrame);
+
+ /**
+ * Get the length of just the EID data from the Eddystone-EID frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return The size in bytes of the EID in the Eddystone-EID frame.
+ */
+ uint8_t getEidLength(uint8_t* rawFrame);
+
+ /**
+ * Set the Adv TX Power in the frame. This is necessary because the adv
+ * Tx Power might be updated independent of the data bytes
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ * @param[in] advPowerLevel
+ * Power level value included in the raw frame.
+ *
+ */
+ void setAdvTxPower(uint8_t* rawFrame, int8_t advTxPower);
+
+
+ /**
+ * Generate the beacon private and public keys. This should be called on
+ * every restart of the beacon.
+ *
+ * @param[out] beaconPrivateEcdhKey
+ * Pointer to the beacon private key array.
+ * @param[out] beaconPublicEcdhKey
+ * Pointer to the beacon public key array.
+ *
+ */
+ int genBeaconKeys(PrivateEcdhKey_t beaconPrivateEcdhKey, PublicEcdhKey_t beaconPublicEcdhKey);
+
+ /**
+ * Update the EID frame. Tests if its time to rotate the EID payload, and if due, calculates and establishes the new value
+ *
+ * @param[in] *rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ * @param[in] *eidIdentityKey
+ * Eid key used to regenerate the EID id.
+ * @param[in] rotationPeriodExp
+ * EID rotation time as an exponent k : 2^k seconds
+ * @param[in] timeSecs
+ * time in seconds
+ *
+ */
+ void update(uint8_t* rawFrame, uint8_t* eidIdentityKey, uint8_t rotationPeriodExp, uint32_t timeSecs);
+
+ /**
+ * genEcdhSharedKey generates the eik value for inclusion in the EID ADV packet
+ *
+ * @param[in] beaconPrivateEcdhKey
+ * The beacon's private ECDH key, generated by genBeaconKeys()
+ * @param[in] beaconPublicEcdhKey
+ * The beacon's public ECDH key, generated by genBeaconKeys()
+ * @param[in] serverPublicEcdhKey
+ * The server's public ECDH key
+ * @param[out] eidIdentityKey
+ * Identity key for this beacon and server combination
+ */
+ int genEcdhSharedKey(PrivateEcdhKey_t beaconPrivateEcdhKey, PublicEcdhKey_t beaconPublicEcdhKey, PublicEcdhKey_t serverPublicEcdhKey, EidIdentityKey_t eidIdentityKey);
+
+ /**
+ * The byte ID of an Eddystone-EID frame.
+ */
+ static const uint8_t FRAME_TYPE_EID = 0x30;
+
+private:
+
+ // Declare context for crypto functions
+ mbedtls_entropy_context entropy;
+ mbedtls_ctr_drbg_context ctr_drbg;
+ mbedtls_ecdh_context ecdh_ctx;
+ mbedtls_md_context_t md_ctx;
+
+ /**
+ * The size (in bytes) of an Eddystone-EID frame.
+ * This is the some of the Eddystone UUID(2 bytes), FrameType, AdvTxPower,
+ * EID Value
+ */
+ static const uint8_t EID_FRAME_LEN = 18;
+ static const uint8_t FRAME_LEN_OFFSET = 0;
+ static const uint8_t EDDYSTONE_UUID_LEN = 2;
+ static const uint8_t EID_DATA_OFFSET = 3;
+ static const uint8_t ADV_FRAME_OFFSET = 1;
+ static const uint8_t EID_VALUE_OFFSET = 5;
+ static const uint8_t EID_HEADER_LEN = 4;
+ static const uint8_t EID_TXPOWER_OFFSET = 4;
+
+ /**
+ * AES128 ECB Encrypts a 16-byte input array with a key, to an output array
+ *
+ * @param[in] *key
+ * The encryption key
+ * @param[in] *input
+ * The input array
+ * @param[in] *output
+ * The output array (contains the encrypted data)
+ */
+ void aes128Encrypt(uint8_t *key, uint8_t *input, uint8_t *output);
+
+};
+
+#endif /* __EIDFRAME_H__ */
diff --git a/implementations/mbed/source/EddystoneService.cpp b/implementations/mbed/source/EddystoneService.cpp
new file mode 100644
index 0000000..95b866b
--- /dev/null
+++ b/implementations/mbed/source/EddystoneService.cpp
@@ -0,0 +1,1331 @@
+/*
+ * Copyright (c) 2006-2016 Google Inc, All Rights Reserved
+ *
+ * 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.
+ */
+
+#include "EddystoneService.h"
+#include "PersistentStorageHelper/ConfigParamsPersistence.h"
+#include "EntropySource/EntropySource.h"
+
+/* Use define zero for production, 1 for testing to allow connection at any time */
+#define DEFAULT_REMAIN_CONNECTABLE 0x01
+
+const char * const EddystoneService::slotDefaultUrls[] = EDDYSTONE_DEFAULT_SLOT_URLS;
+
+// Static timer used as time since boot
+Timer EddystoneService::timeSinceBootTimer;
+
+/*
+ * CONSTRUCTOR #1 Used on 1st boot (after reflash)
+ */
+EddystoneService::EddystoneService(BLE &bleIn,
+ const PowerLevels_t &advTxPowerLevelsIn,
+ const PowerLevels_t &radioTxPowerLevelsIn,
+ event_queue_t &evQ,
+ uint32_t advConfigIntervalIn) :
+ ble(bleIn),
+ operationMode(EDDYSTONE_MODE_NONE),
+ uidFrame(),
+ urlFrame(),
+ tlmFrame(),
+ eidFrame(),
+ tlmBatteryVoltageCallback(NULL),
+ tlmBeaconTemperatureCallback(NULL),
+ radioManagerCallbackHandle(NULL),
+ deviceName(DEFAULT_DEVICE_NAME),
+ eventQueue(evQ),
+ nextEidSlot(0)
+{
+ LOG(("1st Boot: "));
+ LOG((BUILD_VERSION_STR));
+ if (advConfigIntervalIn != 0) {
+ if (advConfigIntervalIn < ble.gap().getMinAdvertisingInterval()) {
+ advConfigInterval = ble.gap().getMinAdvertisingInterval();
+ } else if (advConfigIntervalIn > ble.gap().getMaxAdvertisingInterval()) {
+ advConfigInterval = ble.gap().getMaxAdvertisingInterval();
+ } else {
+ advConfigInterval = advConfigIntervalIn;
+ }
+ }
+ memcpy(radioTxPowerLevels, radioTxPowerLevelsIn, sizeof(PowerLevels_t));
+ memcpy(advTxPowerLevels, advTxPowerLevelsIn, sizeof(PowerLevels_t));
+
+ // 1st Boot so reset everything to factory values
+ LOG(("1st BOOT: "));
+ doFactoryReset(); // includes genBeaconKeys
+
+ LOG(("After FactoryReset: 1st Boot Init: genBeaconKeyRC=%d\r\n", genBeaconKeyRC));
+
+ /* Set the device name at startup */
+ ble.gap().setDeviceName(reinterpret_cast(deviceName));
+}
+
+/*
+ * Constuctor #2: Used on 2nd+ boot: EddystoneService parameters derived from persistent storage
+ */
+EddystoneService::EddystoneService(BLE &bleIn,
+ EddystoneParams_t ¶msIn,
+ const PowerLevels_t &radioTxPowerLevelsIn,
+ event_queue_t &evQ,
+ uint32_t advConfigIntervalIn) :
+ ble(bleIn),
+ operationMode(EDDYSTONE_MODE_NONE),
+ uidFrame(),
+ urlFrame(),
+ tlmFrame(),
+ eidFrame(),
+ tlmBatteryVoltageCallback(NULL),
+ tlmBeaconTemperatureCallback(NULL),
+ radioManagerCallbackHandle(NULL),
+ deviceName(DEFAULT_DEVICE_NAME),
+ eventQueue(evQ),
+ nextEidSlot(0)
+{
+ LOG(("2nd (>=) Boot: "));
+ LOG((BUILD_VERSION_STR));
+ // Init time Params
+ LOG(("Init Params\r\n"));
+ timeSinceBootTimer.start();
+ memcpy(&timeParams, &(paramsIn.timeParams), sizeof(TimeParams_t));
+ LOG(("2nd Boot: Time:"));
+ LOG(("PriorBoots=%lu, SinceBoot=%lu\r\n", timeParams.timeInPriorBoots, timeParams.timeSinceLastBoot));
+ timeParams.timeInPriorBoots = timeParams.timeInPriorBoots + timeParams.timeSinceLastBoot;
+ timeParams.timeSinceLastBoot = getTimeSinceLastBootMs() / 1000;
+ nvmSaveTimeParams();
+
+ // Init gneeral params
+ memcpy(capabilities, paramsIn.capabilities, sizeof(Capability_t));
+ activeSlot = paramsIn.activeSlot;
+ memcpy(radioTxPowerLevels, radioTxPowerLevelsIn, sizeof(PowerLevels_t));
+ memcpy(slotRadioTxPowerLevels, paramsIn.slotRadioTxPowerLevels, sizeof(SlotTxPowerLevels_t));
+ memcpy(advTxPowerLevels, paramsIn.advTxPowerLevels, sizeof(PowerLevels_t));
+ memcpy(slotAdvTxPowerLevels, paramsIn.slotAdvTxPowerLevels, sizeof(SlotTxPowerLevels_t));
+ memcpy(slotAdvIntervals, paramsIn.slotAdvIntervals, sizeof(SlotAdvIntervals_t));
+ lockState = paramsIn.lockState;
+ memcpy(unlockKey, paramsIn.unlockKey, sizeof(Lock_t));
+ memcpy(unlockToken, paramsIn.unlockToken, sizeof(Lock_t));
+ memcpy(challenge, paramsIn.challenge, sizeof(Lock_t));
+ memset(slotCallbackHandles, 0, sizeof(SlotCallbackHandles_t));
+ memcpy(slotStorage, paramsIn.slotStorage, sizeof(SlotStorage_t));
+ memcpy(slotFrameTypes, paramsIn.slotFrameTypes, sizeof(SlotFrameTypes_t));
+ memcpy(slotEidRotationPeriodExps, paramsIn.slotEidRotationPeriodExps, sizeof(SlotEidRotationPeriodExps_t));
+ memcpy(slotEidIdentityKeys, paramsIn.slotEidIdentityKeys, sizeof(SlotEidIdentityKeys_t));
+ // Zero next EID slot rotation times to enforce rotation of each slot on restart
+ memset(slotEidNextRotationTimes, 0, sizeof(SlotEidNextRotationTimes_t));
+ remainConnectable = paramsIn.remainConnectable;
+
+ if (advConfigIntervalIn != 0) {
+ if (advConfigIntervalIn < ble.gap().getMinAdvertisingInterval()) {
+ advConfigInterval = ble.gap().getMinAdvertisingInterval();
+ } else if (advConfigIntervalIn > ble.gap().getMaxAdvertisingInterval()) {
+ advConfigInterval = ble.gap().getMaxAdvertisingInterval();
+ } else {
+ advConfigInterval = advConfigIntervalIn;
+ }
+ }
+
+ // Generate fresh private and public ECDH keys for EID
+ genEIDBeaconKeys();
+
+ // Recompute EID Slot Data
+ for (int slot = 0; slot < MAX_ADV_SLOTS; slot++) {
+ uint8_t* frame = slotToFrame(slot);
+ switch (slotFrameTypes[slot]) {
+ case EDDYSTONE_FRAME_EID:
+ nextEidSlot = slot;
+ eidFrame.setData(frame, slotAdvTxPowerLevels[slot], nullEid);
+ eidFrame.update(frame, slotEidIdentityKeys[slot], slotEidRotationPeriodExps[slot], getTimeSinceFirstBootSecs());
+ break;
+ }
+ }
+
+ /* Set the device name at startup */
+ ble.gap().setDeviceName(reinterpret_cast(deviceName));
+}
+
+// Regenerate the beacon keys
+void EddystoneService::genEIDBeaconKeys(void) {
+ genBeaconKeyRC = -1;
+#ifdef GEN_BEACON_KEYS_AT_INIT
+ memset(privateEcdhKey, 0, 32);
+ memset(publicEcdhKey, 0, 32);
+ genBeaconKeyRC = eidFrame.genBeaconKeys(privateEcdhKey, publicEcdhKey);
+ swapEndianArray(publicEcdhKey, publicEcdhKeyLE, 32);
+#endif
+}
+
+/**
+ * Factory reset all parmeters: used at initial boot, and activated from Char 11
+ */
+void EddystoneService::doFactoryReset(void)
+{
+ // Init Time tracking
+ timeSinceBootTimer.start();
+ timeParams.timeInPriorBoots = 0;
+ timeParams.timeSinceLastBoot = getTimeSinceLastBootMs() / 1000;
+ nvmSaveTimeParams();
+ // Init callbacks
+ memset(slotCallbackHandles, 0, sizeof(SlotCallbackHandles_t));
+ radioManagerCallbackHandle = NULL;
+ memcpy(capabilities, CAPABILITIES_DEFAULT, CAP_HDR_LEN);
+ // Line above leaves powerlevels blank; Line below fills them in
+ memcpy(capabilities + CAP_HDR_LEN, radioTxPowerLevels, sizeof(PowerLevels_t));
+ activeSlot = DEFAULT_SLOT;
+ // Intervals
+ uint16_t buf1[] = EDDYSTONE_DEFAULT_SLOT_INTERVALS;
+ for (int i = 0; i < MAX_ADV_SLOTS; i++) {
+ // Ensure all slot periods are in range
+ buf1[i] = correctAdvertisementPeriod(buf1[i]);
+ }
+ memcpy(slotAdvIntervals, buf1, sizeof(SlotAdvIntervals_t));
+ // Radio and Adv TX Power
+ int8_t buf2[] = EDDYSTONE_DEFAULT_SLOT_TX_POWERS;
+ for (int i = 0; i< MAX_ADV_SLOTS; i++) {
+ slotRadioTxPowerLevels[i] = buf2[i];
+ slotAdvTxPowerLevels[i] = advTxPowerLevels[radioTxPowerToIndex(buf2[i])];
+ }
+ // Lock
+ lockState = UNLOCKED;
+ uint8_t defKeyBuf[] = EDDYSTONE_DEFAULT_UNLOCK_KEY;
+ memcpy(unlockKey, defKeyBuf, sizeof(Lock_t));
+ memset(unlockToken, 0, sizeof(Lock_t));
+ memset(challenge, 0, sizeof(Lock_t)); // NOTE: challenge is randomized on first unlockChar read;
+
+ // Generate ECDH Beacon Key Pair (Private/Public)
+ genEIDBeaconKeys();
+
+ memcpy(slotEidIdentityKeys, slotDefaultEidIdentityKeys, sizeof(SlotEidIdentityKeys_t));
+ uint8_t buf4[] = EDDYSTONE_DEFAULT_SLOT_EID_ROTATION_PERIOD_EXPS;
+ memcpy(slotEidRotationPeriodExps, buf4, sizeof(SlotEidRotationPeriodExps_t));
+ memset(slotEidNextRotationTimes, 0, sizeof(SlotEidNextRotationTimes_t));
+ // Slot Data Type Defaults
+ uint8_t buf3[] = EDDYSTONE_DEFAULT_SLOT_TYPES;
+ memcpy(slotFrameTypes, buf3, sizeof(SlotFrameTypes_t));
+ // Initialize Slot Data Defaults
+ int eidSlot;
+ for (int slot = 0; slot < MAX_ADV_SLOTS; slot++) {
+ uint8_t* frame = slotToFrame(slot);
+ switch (slotFrameTypes[slot]) {
+ case EDDYSTONE_FRAME_UID:
+ uidFrame.setData(frame, slotAdvTxPowerLevels[slot], reinterpret_cast(slotDefaultUids[slot]));
+ break;
+ case EDDYSTONE_FRAME_URL:
+ urlFrame.setUnencodedUrlData(frame, slotAdvTxPowerLevels[slot], slotDefaultUrls[slot]);
+ break;
+ case EDDYSTONE_FRAME_TLM:
+ tlmFrame.setTLMData(TLMFrame::DEFAULT_TLM_VERSION);
+ tlmFrame.setData(frame);
+ eidSlot = getEidSlot();
+ if (eidSlot != NO_EID_SLOT_SET) {
+ LOG(("EID slot Set in FactoryReset\r\n"));
+ tlmFrame.encryptData(frame, slotEidIdentityKeys[eidSlot], slotEidRotationPeriodExps[eidSlot], getTimeSinceFirstBootSecs());
+ }
+ break;
+ case EDDYSTONE_FRAME_EID:
+ nextEidSlot = slot;
+ eidFrame.setData(frame, slotAdvTxPowerLevels[slot], nullEid);
+ eidFrame.update(frame, slotEidIdentityKeys[slot], slotEidRotationPeriodExps[slot], getTimeSinceFirstBootSecs());
+ break;
+ }
+ }
+
+#ifdef DONT_REMAIN_CONNECTABLE
+ remainConnectable = REMAIN_CONNECTABLE_UNSET;
+#else
+ remainConnectable = REMAIN_CONNECTABLE_SET;
+#endif
+ factoryReset = false;
+}
+
+/* Setup callback to update BatteryVoltage in TLM frame */
+void EddystoneService::onTLMBatteryVoltageUpdate(TlmUpdateCallback_t tlmBatteryVoltageCallbackIn)
+{
+ tlmBatteryVoltageCallback = tlmBatteryVoltageCallbackIn;
+}
+
+/* Setup callback to update BeaconTemperature in TLM frame */
+void EddystoneService::onTLMBeaconTemperatureUpdate(TlmUpdateCallback_t tlmBeaconTemperatureCallbackIn)
+{
+ tlmBeaconTemperatureCallback = tlmBeaconTemperatureCallbackIn;
+}
+
+EddystoneService::EddystoneError_t EddystoneService::startEddystoneBeaconAdvertisements(void)
+{
+ stopEddystoneBeaconAdvertisements();
+
+ bool intervalValidFlag = false;
+ for (int i = 0; i < MAX_ADV_SLOTS; i++) {
+ if (slotAdvIntervals[i] != 0) {
+ intervalValidFlag = true;
+ }
+ }
+
+ if (!intervalValidFlag) {
+ /* Nothing to do, the period is 0 for all frames */
+ return EDDYSTONE_ERROR_INVALID_ADVERTISING_INTERVAL;
+ }
+
+ // In case left over from Config Adv Mode
+ ble.gap().clearScanResponse();
+
+ operationMode = EDDYSTONE_MODE_BEACON;
+
+ /* Configure advertisements initially at power of active slot*/
+ ble.gap().setTxPower(slotRadioTxPowerLevels[activeSlot]);
+
+ if (remainConnectable) {
+ ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
+ } else {
+ ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_NON_CONNECTABLE_UNDIRECTED);
+ }
+ ble.gap().setAdvertisingInterval(ble.gap().getMaxAdvertisingInterval());
+
+ /* Make sure the queue is currently empty */
+ advFrameQueue.reset();
+ /* Setup callbacks to periodically add frames to be advertised to the queue and
+ * add initial frame so that we have something to advertise on startup */
+ for (int slot = 0; slot < MAX_ADV_SLOTS; slot++) {
+ uint8_t* frame = slotToFrame(slot);
+ if (slotAdvIntervals[slot] && testValidFrame(frame)) {
+ advFrameQueue.push(slot);
+ slotCallbackHandles[slot] = eventQueue.post_every(
+ &EddystoneService::enqueueFrame, this, slot,
+ slotAdvIntervals[slot] /* ms */
+ );
+ }
+ }
+ /* Start advertising */
+ manageRadio();
+
+ return EDDYSTONE_ERROR_NONE;
+}
+
+ble_error_t EddystoneService::setCompleteDeviceName(const char *deviceNameIn)
+{
+ /* Make sure the device name is safe */
+ ble_error_t error = ble.gap().setDeviceName(reinterpret_cast(deviceNameIn));
+ if (error == BLE_ERROR_NONE) {
+ deviceName = deviceNameIn;
+ if (operationMode == EDDYSTONE_MODE_CONFIG) {
+ /* Need to update the advertising packets to the new name */
+ setupEddystoneConfigScanResponse();
+ }
+ }
+
+ return error;
+}
+
+/* It is not the responsibility of the Eddystone implementation to store
+ * the configured parameters in persistent storage since this is
+ * platform-specific. So we provide this function that returns the
+ * configured values that need to be stored and the main application
+ * takes care of storing them.
+ */
+void EddystoneService::getEddystoneParams(EddystoneParams_t ¶ms)
+{
+ // Time
+ timeParams.timeSinceLastBoot = getTimeSinceLastBootMs() / 1000;
+ memcpy(&(params.timeParams), &timeParams, sizeof(TimeParams_t));
+ // Capabilities
+ memcpy(params.capabilities, capabilities, sizeof(Capability_t));
+ // Active Slot
+ params.activeSlot = activeSlot;
+ // Intervals
+ memcpy(params.slotAdvIntervals, slotAdvIntervals, sizeof(SlotAdvIntervals_t));
+ // Power Levels
+ memcpy(params.radioTxPowerLevels, radioTxPowerLevels, sizeof(PowerLevels_t));
+ memcpy(params.advTxPowerLevels, advTxPowerLevels, sizeof(PowerLevels_t));
+ // Slot Power Levels
+ memcpy(params.slotRadioTxPowerLevels, slotRadioTxPowerLevels, sizeof(MAX_ADV_SLOTS));
+ memcpy(params.slotAdvTxPowerLevels, slotAdvTxPowerLevels, sizeof(MAX_ADV_SLOTS));
+ // Lock
+ params.lockState = lockState;
+ memcpy(params.unlockKey, unlockKey, sizeof(Lock_t));
+ memcpy(params.unlockToken, unlockToken, sizeof(Lock_t));
+ memcpy(params.challenge, challenge, sizeof(Lock_t));
+ // Slots
+ memcpy(params.slotFrameTypes, slotFrameTypes, sizeof(SlotFrameTypes_t));
+ memcpy(params.slotStorage, slotStorage, sizeof(SlotStorage_t));
+ memcpy(params.slotEidRotationPeriodExps, slotEidRotationPeriodExps, sizeof(SlotEidRotationPeriodExps_t));
+ memcpy(params.slotEidIdentityKeys, slotEidIdentityKeys, sizeof(SlotEidIdentityKeys_t));
+ // Testing and Management
+ params.remainConnectable = remainConnectable;
+}
+
+void EddystoneService::swapAdvertisedFrame(int slot)
+{
+ uint8_t* frame = slotToFrame(slot);
+ uint8_t frameType = slotFrameTypes[slot];
+ uint32_t timeSecs = getTimeSinceFirstBootSecs();
+ switch (frameType) {
+ case EDDYSTONE_FRAME_UID:
+ updateAdvertisementPacket(uidFrame.getAdvFrame(frame), uidFrame.getAdvFrameLength(frame));
+ break;
+ case EDDYSTONE_FRAME_URL:
+ updateAdvertisementPacket(urlFrame.getAdvFrame(frame), urlFrame.getAdvFrameLength(frame));
+ break;
+ case EDDYSTONE_FRAME_TLM:
+ updateRawTLMFrame(frame);
+ updateAdvertisementPacket(tlmFrame.getAdvFrame(frame), tlmFrame.getAdvFrameLength(frame));
+ break;
+ case EDDYSTONE_FRAME_EID:
+ // only update the frame if the rotation period is due
+ if (timeSecs >= slotEidNextRotationTimes[slot]) {
+ eidFrame.update(frame, slotEidIdentityKeys[slot], slotEidRotationPeriodExps[slot], timeSecs);
+ slotEidNextRotationTimes[slot] = timeSecs + (1 << slotEidRotationPeriodExps[slot]);
+ // select a new random MAC address so the beacon is not trackable
+ setRandomMacAddress();
+ // Store in NVM in case the beacon loses power
+ nvmSaveTimeParams();
+ LOG(("EID ROTATED: Time=%lu\r\n", timeSecs));
+ }
+ updateAdvertisementPacket(eidFrame.getAdvFrame(frame), eidFrame.getAdvFrameLength(frame));
+ break;
+ default:
+ //Some error occurred
+ error("Frame to swap in does not specify a valid type");
+ break;
+ }
+ ble.gap().setTxPower(slotRadioTxPowerLevels[slot]);
+}
+
+
+/* Helper function that calls user-defined functions to update Battery Voltage and Temperature (if available),
+ * then updates the raw frame data and finally updates the actual advertised packet. This operation must be
+ * done fairly often because the TLM frame TimeSinceBoot must have a 0.1 secs resolution according to the
+ * Eddystone specification.
+ */
+void EddystoneService::updateRawTLMFrame(uint8_t* frame)
+{
+ if (tlmBeaconTemperatureCallback != NULL) {
+ tlmFrame.updateBeaconTemperature((*tlmBeaconTemperatureCallback)(tlmFrame.getBeaconTemperature()));
+ }
+ if (tlmBatteryVoltageCallback != NULL) {
+ tlmFrame.updateBatteryVoltage((*tlmBatteryVoltageCallback)(tlmFrame.getBatteryVoltage()));
+ }
+ tlmFrame.updateTimeSinceLastBoot(getTimeSinceLastBootMs());
+ tlmFrame.setData(frame);
+ int slot = getEidSlot();
+ LOG(("TLMHelper Method slot=%d\r\n", slot));
+ if (slot != NO_EID_SLOT_SET) {
+ LOG(("TLMHelper: Before Encrypting TLM\r\n"));
+ tlmFrame.encryptData(frame, slotEidIdentityKeys[slot], slotEidRotationPeriodExps[slot], getTimeSinceFirstBootSecs());
+ LOG(("TLMHelper: Before Encrypting TLM\r\n"));
+ }
+}
+
+void EddystoneService::updateAdvertisementPacket(const uint8_t* rawFrame, size_t rawFrameLength)
+{
+ ble.gap().clearAdvertisingPayload();
+ ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
+ ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, EDDYSTONE_UUID, sizeof(EDDYSTONE_UUID));
+ ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::SERVICE_DATA, rawFrame, rawFrameLength);
+}
+
+uint8_t* EddystoneService::slotToFrame(int slot)
+{
+ return reinterpret_cast(&slotStorage[slot * sizeof(Slot_t)]);
+}
+
+void EddystoneService::enqueueFrame(int slot)
+{
+ advFrameQueue.push(slot);
+ if (!radioManagerCallbackHandle) {
+ /* Advertising stopped and there is not callback posted in the event queue. Just
+ * execute the manager to resume advertising */
+ manageRadio();
+ }
+}
+
+void EddystoneService::manageRadio(void)
+{
+ uint8_t slot;
+ uint64_t startTimeManageRadio = getTimeSinceLastBootMs();
+
+ /* Signal that there is currently no callback posted */
+ radioManagerCallbackHandle = NULL;
+
+ if (advFrameQueue.pop(slot)) {
+ /* We have something to advertise */
+ if (ble.gap().getState().advertising) {
+ ble.gap().stopAdvertising();
+ }
+ swapAdvertisedFrame(slot);
+ ble.gap().startAdvertising();
+
+ /* Increase the advertised packet count in TLM frame */
+ tlmFrame.updatePduCount();
+
+ /* Post a callback to itself to stop the advertisement or pop the next
+ * frame from the queue. However, take into account the time taken to
+ * swap in this frame. */
+ radioManagerCallbackHandle = eventQueue.post_in(
+ &EddystoneService::manageRadio, this,
+ ble.gap().getMinNonConnectableAdvertisingInterval() - (getTimeSinceLastBootMs() - startTimeManageRadio) /* ms */
+ );
+ } else if (ble.gap().getState().advertising) {
+ /* Nothing else to advertise, stop advertising and do not schedule any callbacks */
+ ble.gap().stopAdvertising();
+ }
+}
+
+void EddystoneService::startEddystoneConfigService(void)
+{
+ uint16_t beAdvInterval = swapEndian(slotAdvIntervals[activeSlot]);
+ int8_t radioTxPower = slotRadioTxPowerLevels[activeSlot];
+ int8_t advTxPower = slotAdvTxPowerLevels[activeSlot];
+ uint8_t* slotData = slotToFrame(activeSlot) + 1;
+ aes128Encrypt(unlockKey, slotEidIdentityKeys[activeSlot], encryptedEidIdentityKey);
+
+ capabilitiesChar = new ReadOnlyArrayGattCharacteristic(UUID_CAPABILITIES_CHAR, capabilities);
+ activeSlotChar = new ReadWriteGattCharacteristic(UUID_ACTIVE_SLOT_CHAR, &activeSlot);
+ advIntervalChar = new ReadWriteGattCharacteristic(UUID_ADV_INTERVAL_CHAR, &beAdvInterval);
+ radioTxPowerChar = new ReadWriteGattCharacteristic(UUID_RADIO_TX_POWER_CHAR, &radioTxPower);
+ advTxPowerChar = new ReadWriteGattCharacteristic(UUID_ADV_TX_POWER_CHAR, &advTxPower);
+ lockStateChar = new GattCharacteristic(UUID_LOCK_STATE_CHAR, &lockState, sizeof(uint8_t), sizeof(LockState_t), GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ | GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE);
+ unlockChar = new ReadWriteArrayGattCharacteristic(UUID_UNLOCK_CHAR, unlockToken);
+ publicEcdhKeyChar = new GattCharacteristic(UUID_PUBLIC_ECDH_KEY_CHAR, publicEcdhKey, 0, sizeof(PublicEcdhKey_t), GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ);
+ eidIdentityKeyChar = new GattCharacteristic(UUID_EID_IDENTITY_KEY_CHAR, encryptedEidIdentityKey, 0, sizeof(EidIdentityKey_t), GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ);
+ advSlotDataChar = new GattCharacteristic(UUID_ADV_SLOT_DATA_CHAR, slotData, 0, 34, GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ | GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_WRITE);
+ factoryResetChar = new WriteOnlyGattCharacteristic(UUID_FACTORY_RESET_CHAR, &factoryReset);
+ remainConnectableChar = new ReadWriteGattCharacteristic(UUID_REMAIN_CONNECTABLE_CHAR, &remainConnectable);
+
+ // CHAR-1 capabilities (READ ONLY)
+ capabilitiesChar->setReadAuthorizationCallback(this, &EddystoneService::readBasicTestLockAuthorizationCallback);
+ // CHAR-2 Active Slot
+ activeSlotChar->setReadAuthorizationCallback(this, &EddystoneService::readBasicTestLockAuthorizationCallback);
+ activeSlotChar->setWriteAuthorizationCallback(this, &EddystoneService::writeActiveSlotAuthorizationCallback);
+ // CHAR-3 Adv Interval
+ advIntervalChar->setReadAuthorizationCallback(this, &EddystoneService::readAdvIntervalAuthorizationCallback);
+ advIntervalChar->setWriteAuthorizationCallback(this, &EddystoneService::writeBasicAuthorizationCallback);
+ // CHAR-4 Radio TX Power
+ radioTxPowerChar->setReadAuthorizationCallback(this, &EddystoneService::readRadioTxPowerAuthorizationCallback);
+ radioTxPowerChar->setWriteAuthorizationCallback(this, &EddystoneService::writeBasicAuthorizationCallback);
+ // CHAR-5
+ advTxPowerChar->setReadAuthorizationCallback(this, &EddystoneService::readAdvTxPowerAuthorizationCallback);
+ advTxPowerChar->setWriteAuthorizationCallback(this, &EddystoneService::writeBasicAuthorizationCallback);
+ // CHAR-6 Lock State
+ lockStateChar->setWriteAuthorizationCallback(this, &EddystoneService::writeLockStateAuthorizationCallback);
+ // CHAR-7 Unlock
+ unlockChar->setReadAuthorizationCallback(this, &EddystoneService::readUnlockAuthorizationCallback);
+ unlockChar->setWriteAuthorizationCallback(this, &EddystoneService::writeUnlockAuthorizationCallback);
+ // CHAR-8 Public Ecdh Key (READ ONLY)
+ publicEcdhKeyChar->setReadAuthorizationCallback(this, &EddystoneService::readPublicEcdhKeyAuthorizationCallback);
+ // CHAR-9 EID Identity Key (READ ONLY)
+ eidIdentityKeyChar->setReadAuthorizationCallback(this, &EddystoneService::readEidIdentityAuthorizationCallback);
+ // CHAR-10 Adv Slot Data
+ advSlotDataChar->setReadAuthorizationCallback(this, &EddystoneService::readDataAuthorizationCallback);
+ advSlotDataChar->setWriteAuthorizationCallback(this, &EddystoneService::writeVarLengthDataAuthorizationCallback);
+ // CHAR-11 Factory Reset
+ factoryResetChar->setReadAuthorizationCallback(this, &EddystoneService::readBasicTestLockAuthorizationCallback);
+ factoryResetChar->setWriteAuthorizationCallback(this, &EddystoneService::writeBasicAuthorizationCallback);
+ // CHAR-12 Remain Connectable
+ remainConnectableChar->setReadAuthorizationCallback(this, &EddystoneService::readBasicTestLockAuthorizationCallback);
+ remainConnectableChar->setWriteAuthorizationCallback(this, &EddystoneService::writeBasicAuthorizationCallback);
+
+ // Create pointers to all characteristics in the GATT service
+ charTable[0] = capabilitiesChar;
+ charTable[1] = activeSlotChar;
+ charTable[2] = advIntervalChar;
+ charTable[3] = radioTxPowerChar;
+ charTable[4] = advTxPowerChar;
+ charTable[5] = lockStateChar;
+ charTable[6] = unlockChar;
+ charTable[7] = publicEcdhKeyChar;
+ charTable[8] = eidIdentityKeyChar;
+ charTable[9] = advSlotDataChar;
+ charTable[10] = factoryResetChar;
+ charTable[11] = remainConnectableChar;
+
+ GattService configService(UUID_ES_BEACON_SERVICE, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
+
+ ble.gattServer().addService(configService);
+ ble.gattServer().onDataWritten(this, &EddystoneService::onDataWrittenCallback);
+ updateCharacteristicValues();
+}
+
+
+void EddystoneService::freeConfigCharacteristics(void)
+{
+ delete capabilitiesChar;
+ delete activeSlotChar;
+ delete advIntervalChar;
+ delete radioTxPowerChar;
+ delete advTxPowerChar;
+ delete lockStateChar;
+ delete unlockChar;
+ delete publicEcdhKeyChar;
+ delete eidIdentityKeyChar;
+ delete advSlotDataChar;
+ delete factoryResetChar;
+ delete remainConnectableChar;
+}
+
+void EddystoneService::stopEddystoneBeaconAdvertisements(void)
+{
+ /* Unschedule callbacks */
+
+ for (int slot = 0; slot < MAX_ADV_SLOTS; slot++) {
+ if (slotCallbackHandles[slot]) {
+ eventQueue.cancel(slotCallbackHandles[slot]);
+ slotCallbackHandles[slot] = NULL;
+ }
+ }
+
+ if (radioManagerCallbackHandle) {
+ eventQueue.cancel(radioManagerCallbackHandle);
+ radioManagerCallbackHandle = NULL;
+ }
+
+ /* Stop any current Advs (ES Config or Beacon) */
+ BLE::Instance().gap().stopAdvertising();
+}
+
+/*
+ * Internal helper function used to update the GATT database following any
+ * change to the internal state of the service object.
+ */
+void EddystoneService::updateCharacteristicValues(void)
+{
+ // Init variables for update
+ uint16_t beAdvInterval = swapEndian(slotAdvIntervals[activeSlot]);
+ int8_t radioTxPower = slotRadioTxPowerLevels[activeSlot];
+ int8_t advTxPower = slotAdvTxPowerLevels[activeSlot];
+ uint8_t* frame = slotToFrame(activeSlot);
+ uint8_t slotLength = 0;
+ uint8_t* slotData = NULL;
+ memset(encryptedEidIdentityKey, 0, sizeof(encryptedEidIdentityKey));
+
+ switch(slotFrameTypes[activeSlot]) {
+ case EDDYSTONE_FRAME_UID:
+ slotLength = uidFrame.getDataLength(frame);
+ slotData = uidFrame.getData(frame);
+ break;
+ case EDDYSTONE_FRAME_URL:
+ slotLength = urlFrame.getDataLength(frame);
+ slotData = urlFrame.getData(frame);
+ break;
+ case EDDYSTONE_FRAME_TLM:
+ updateRawTLMFrame(frame);
+ slotLength = tlmFrame.getDataLength(frame);
+ slotData = tlmFrame.getData(frame);
+ break;
+ case EDDYSTONE_FRAME_EID:
+ slotLength = eidFrame.getDataLength(frame);
+ slotData = eidFrame.getData(frame);
+ aes128Encrypt(unlockKey, slotEidIdentityKeys[activeSlot], encryptedEidIdentityKey);
+ break;
+ }
+
+ ble.gattServer().write(capabilitiesChar->getValueHandle(), reinterpret_cast(capabilities), sizeof(Capability_t));
+ ble.gattServer().write(activeSlotChar->getValueHandle(), &activeSlot, sizeof(uint8_t));
+ ble.gattServer().write(advIntervalChar->getValueHandle(), reinterpret_cast(&beAdvInterval), sizeof(uint16_t));
+ ble.gattServer().write(radioTxPowerChar->getValueHandle(), reinterpret_cast(&radioTxPower), sizeof(int8_t));
+ ble.gattServer().write(advTxPowerChar->getValueHandle(), reinterpret_cast(&advTxPower), sizeof(int8_t));
+ ble.gattServer().write(lockStateChar->getValueHandle(), &lockState, sizeof(uint8_t));
+ ble.gattServer().write(unlockChar->getValueHandle(), unlockToken, sizeof(Lock_t));
+ ble.gattServer().write(publicEcdhKeyChar->getValueHandle(), reinterpret_cast(publicEcdhKey), sizeof(PublicEcdhKey_t));
+ ble.gattServer().write(eidIdentityKeyChar->getValueHandle(), reinterpret_cast(encryptedEidIdentityKey), sizeof(EidIdentityKey_t));
+ ble.gattServer().write(advSlotDataChar->getValueHandle(), slotData, slotLength);
+ ble.gattServer().write(factoryResetChar->getValueHandle(), &factoryReset, sizeof(uint8_t));
+ ble.gattServer().write(remainConnectableChar->getValueHandle(), &remainConnectable, sizeof(uint8_t));
+}
+
+EddystoneService::EddystoneError_t EddystoneService::startEddystoneConfigAdvertisements(void)
+{
+ stopEddystoneBeaconAdvertisements();
+
+ if (advConfigInterval == 0) {
+ // Nothing to do, the advertisement interval is 0
+ return EDDYSTONE_ERROR_INVALID_ADVERTISING_INTERVAL;
+ }
+
+ operationMode = EDDYSTONE_MODE_CONFIG;
+
+ ble.gap().clearAdvertisingPayload();
+
+ /* Accumulate the new payload */
+ // Add the Flags param
+ ble.gap().accumulateAdvertisingPayload(
+ GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE
+ );
+#ifdef INCLUDE_CONFIG_URL
+ // Add the Eddystone 16-bit Service ID
+ ble.gap().accumulateAdvertisingPayload(
+ GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS,
+ EDDYSTONE_UUID,
+ sizeof(EDDYSTONE_UUID)
+ );
+#endif
+ /* UUID is in different order in the ADV frame (!) */
+ uint8_t reversedServiceUUID[sizeof(UUID_ES_BEACON_SERVICE)];
+ for (size_t i = 0; i < sizeof(UUID_ES_BEACON_SERVICE); i++) {
+ reversedServiceUUID[i] = UUID_ES_BEACON_SERVICE[sizeof(UUID_ES_BEACON_SERVICE) - i - 1];
+ }
+ ble.gap().accumulateAdvertisingPayload(
+ GapAdvertisingData::COMPLETE_LIST_128BIT_SERVICE_IDS,
+ reversedServiceUUID,
+ sizeof(reversedServiceUUID)
+ );
+ // Add Generic Appearance Tag
+ ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::GENERIC_TAG);
+ setupEddystoneConfigScanResponse();
+
+ ble.gap().setTxPower(radioTxPowerLevels[sizeof(PowerLevels_t)-1]); // Max Power for Config
+ ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
+ ble.gap().setAdvertisingInterval(advConfigInterval);
+ ble.gap().startAdvertising();
+
+ return EDDYSTONE_ERROR_NONE;
+}
+
+void EddystoneService::setupEddystoneConfigScanResponse(void)
+{
+ ble.gap().clearScanResponse();
+ // Add LOCAL NAME (indicating the Eddystone Version)
+ ble.gap().accumulateScanResponse(
+ GapAdvertisingData::COMPLETE_LOCAL_NAME,
+ reinterpret_cast(deviceName),
+ strlen(deviceName)
+ );
+#ifdef INCLUDE_CONFIG_URL
+ // Add SERVICE DATA for a PhyWeb Config URL
+ uint8_t configFrame[URLFrame::ENCODED_BUF_SIZE];
+ int encodedUrlLen = URLFrame::encodeURL(configFrame + CONFIG_FRAME_HDR_LEN, EDDYSTONE_CONFIG_URL);
+ uint8_t advPower = advTxPowerLevels[sizeof(PowerLevels_t)-1] & 0xFF;
+ uint8_t configFrameHdr[CONFIG_FRAME_HDR_LEN] = {0, 0, URLFrame::FRAME_TYPE_URL, advPower};
+ // ++ Fill in the Eddystone Service UUID in the HDR
+ memcpy(configFrameHdr, EDDYSTONE_UUID, sizeof(EDDYSTONE_UUID));
+ // ++ Copy the HDR to the config frame
+ memcpy(configFrame, configFrameHdr, CONFIG_FRAME_HDR_LEN);
+ ble.gap().accumulateScanResponse(
+ GapAdvertisingData::SERVICE_DATA,
+ configFrame,
+ CONFIG_FRAME_HDR_LEN + encodedUrlLen
+ );
+#else
+ // Add TRANSMIT POWER
+ ble.gap().accumulateScanResponse(
+ GapAdvertisingData::TX_POWER_LEVEL,
+ reinterpret_cast(&advTxPowerLevels[sizeof(PowerLevels_t)-1]),
+ sizeof(uint8_t)
+ );
+#endif
+}
+
+/* WRITE AUTHORIZATION */
+
+void EddystoneService::writeUnlockAuthorizationCallback(GattWriteAuthCallbackParams *authParams)
+{
+ if (lockState == UNLOCKED) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_WRITE_NOT_PERMITTED;
+ } else if (authParams->len != sizeof(Lock_t)) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_INVALID_ATT_VAL_LENGTH;
+ } else if (authParams->offset != 0) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_INVALID_OFFSET;
+ } else if (memcmp(authParams->data, unlockToken, sizeof(Lock_t)) != 0) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_WRITE_NOT_PERMITTED;
+ } else {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_SUCCESS;
+ }
+}
+
+void EddystoneService::writeVarLengthDataAuthorizationCallback(GattWriteAuthCallbackParams *authParams)
+{
+ if (lockState == LOCKED) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_WRITE_NOT_PERMITTED;
+ } else if (authParams->len > 34) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_INVALID_ATT_VAL_LENGTH;
+ } else {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_SUCCESS;
+ }
+}
+
+
+void EddystoneService::writeLockStateAuthorizationCallback(GattWriteAuthCallbackParams *authParams)
+{
+ if (lockState == LOCKED) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_WRITE_NOT_PERMITTED;
+ } else if ((authParams->len != sizeof(uint8_t)) && (authParams->len != (sizeof(uint8_t) + sizeof(Lock_t)))) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_INVALID_ATT_VAL_LENGTH;
+ } else if (authParams->offset != 0) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_INVALID_OFFSET;
+ } else {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_SUCCESS;
+ }
+}
+
+template
+void EddystoneService::writeBasicAuthorizationCallback(GattWriteAuthCallbackParams *authParams)
+{
+ if (lockState == LOCKED) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_WRITE_NOT_PERMITTED;
+ } else if (authParams->len != sizeof(T)) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_INVALID_ATT_VAL_LENGTH;
+ } else if (authParams->offset != 0) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_INVALID_OFFSET;
+ } else {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_SUCCESS;
+ }
+}
+
+template
+void EddystoneService::writeActiveSlotAuthorizationCallback(GattWriteAuthCallbackParams *authParams)
+{
+ if (lockState == LOCKED) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_WRITE_NOT_PERMITTED;
+ } else if (authParams->len != sizeof(T)) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_INVALID_ATT_VAL_LENGTH;
+ } else if (*(authParams->data) > MAX_ADV_SLOTS -1) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_INVALID_ATT_VAL_LENGTH;
+ } else if (authParams->offset != 0) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_INVALID_OFFSET;
+ } else {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_SUCCESS;
+ }
+}
+
+/* READ AUTHORIZTION */
+
+void EddystoneService::readBasicTestLockAuthorizationCallback(GattReadAuthCallbackParams *authParams)
+{
+ LOG(("\r\nDO READ BASIC TEST LOCK slot=%d\r\n", activeSlot));
+ if (lockState == LOCKED) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_READ_NOT_PERMITTED;
+ } else {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_SUCCESS;
+ }
+}
+
+void EddystoneService::readEidIdentityAuthorizationCallback(GattReadAuthCallbackParams *authParams)
+{
+ LOG(("\r\nDO READ EID IDENTITY slot=%d\r\n", activeSlot));
+ aes128Encrypt(unlockKey, slotEidIdentityKeys[activeSlot], encryptedEidIdentityKey);
+ int sum = 0;
+ // Test if the IdentityKey is all zeros for this slot
+ for (uint8_t i = 0; i < sizeof(EidIdentityKey_t); i++) {
+ sum = sum + slotEidIdentityKeys[activeSlot][i];
+ }
+ ble.gattServer().write(eidIdentityKeyChar->getValueHandle(), encryptedEidIdentityKey, sizeof(EidIdentityKey_t));
+
+ // When the array is all zeros, the key has not been set, so return fault
+ if ((lockState == LOCKED) || (sum == 0)) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_READ_NOT_PERMITTED;
+ } else {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_SUCCESS;
+ }
+}
+
+void EddystoneService::readPublicEcdhKeyAuthorizationCallback(GattReadAuthCallbackParams *authParams)
+{
+ LOG(("\r\nDO READ BEACON PUBLIC ECDH KEY (LE) slot=%d\r\n", activeSlot));
+
+ ble.gattServer().write(publicEcdhKeyChar->getValueHandle(), publicEcdhKeyLE, sizeof(PublicEcdhKey_t));
+
+ // When the array is all zeros, the key has not been set, so return fault
+ if (lockState == LOCKED) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_READ_NOT_PERMITTED;
+ } else {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_SUCCESS;
+ }
+}
+
+void EddystoneService::readDataAuthorizationCallback(GattReadAuthCallbackParams *authParams)
+{
+ LOG(("\r\nDO READ ADV-DATA : slot=%d\r\n", activeSlot));
+ uint8_t frameType = slotFrameTypes[activeSlot];
+ uint8_t* frame = slotToFrame(activeSlot);
+ uint8_t slotLength = 1;
+ uint8_t buf[14] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0};
+ uint8_t* slotData = buf;
+
+ if (lockState == LOCKED) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_READ_NOT_PERMITTED;
+ return;
+ }
+ LOG(("IN READ ADV-DATA AFTER LOCK TEST frameType=%d\r\n", frameType));
+ if (testValidFrame(frame) ) { // Check the frame has valid data before proceeding
+ switch(frameType) {
+ case EDDYSTONE_FRAME_UID:
+ LOG(("READ ADV-DATA UID SLOT DATA slot=%d\r\n", activeSlot));
+ slotLength = uidFrame.getDataLength(frame);
+ slotData = uidFrame.getData(frame);
+ break;
+ case EDDYSTONE_FRAME_URL:
+ LOG(("READ ADV-DATA URL SLOT DATA slot=%d\r\n", activeSlot));
+ slotLength = urlFrame.getDataLength(frame);
+ slotData = urlFrame.getData(frame);
+ break;
+ case EDDYSTONE_FRAME_TLM:
+ LOG(("READ ADV-DATA TLM SLOT DATA slot=%d\r\n", activeSlot));
+ updateRawTLMFrame(frame);
+ slotLength = tlmFrame.getDataLength(frame);
+ slotData = tlmFrame.getData(frame);
+ LOG(("READ ADV-DATA AFTER T/E TLM length=%d\r\n", slotLength));
+ LOG(("Data=")); logPrintHex(slotData, 18);
+ break;
+ case EDDYSTONE_FRAME_EID:
+ LOG(("READ ADV-DATA EID SLOT DATA slot=%d\r\n", activeSlot));
+ slotLength = 14;
+ buf[0] = EIDFrame::FRAME_TYPE_EID;
+ buf[1] = slotEidRotationPeriodExps[activeSlot];
+ // Add time as a big endian 32 bit number
+ uint32_t timeSecs = getTimeSinceFirstBootSecs();
+ buf[2] = (timeSecs >> 24) & 0xff;
+ buf[3] = (timeSecs >> 16) & 0xff;
+ buf[4] = (timeSecs >> 8) & 0xff;
+ buf[5] = timeSecs & 0xff;
+ memcpy(buf + 6, eidFrame.getEid(frame), 8);
+ slotData = buf;
+ break;
+ }
+ }
+ LOG(("IN READ ADV-DATA AFTER FRAME PROCESSING slot=%d\r\n", activeSlot));
+ ble.gattServer().write(advSlotDataChar->getValueHandle(), slotData, slotLength);
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_SUCCESS;
+}
+
+bool EddystoneService::testValidFrame(uint8_t* frame) {
+ return (frame[0] != 0 ) ? true : false;
+}
+
+void EddystoneService::readUnlockAuthorizationCallback(GattReadAuthCallbackParams *authParams)
+{
+ LOG(("\r\nDO READ UNLOCK slot=%d\r\n", activeSlot));
+ if (lockState == UNLOCKED) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_READ_NOT_PERMITTED;
+ return;
+ }
+ // Update the challenge ready for the characteristic read
+ generateRandom(challenge, sizeof(Lock_t));
+ aes128Encrypt(unlockKey, challenge, unlockToken);
+ ble.gattServer().write(unlockChar->getValueHandle(), reinterpret_cast(challenge), sizeof(Lock_t));
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_SUCCESS;
+}
+
+void EddystoneService::readAdvIntervalAuthorizationCallback(GattReadAuthCallbackParams *authParams)
+{
+ LOG(("\r\nDO READ ADV INTERVAL slot=%d\r\n", activeSlot));
+ if (lockState == LOCKED) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_READ_NOT_PERMITTED;
+ return;
+ }
+ uint16_t beAdvInterval = swapEndian(slotAdvIntervals[activeSlot]);
+ ble.gattServer().write(advIntervalChar->getValueHandle(), reinterpret_cast(&beAdvInterval), sizeof(uint16_t));
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_SUCCESS;
+}
+
+void EddystoneService::readRadioTxPowerAuthorizationCallback(GattReadAuthCallbackParams *authParams)
+{
+ LOG(("\r\nDO READ RADIO TXPOWER slot=%d\r\n", activeSlot));
+ if (lockState == LOCKED) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_READ_NOT_PERMITTED;
+ return;
+ }
+ int8_t radioTxPower = slotRadioTxPowerLevels[activeSlot];
+ ble.gattServer().write(radioTxPowerChar->getValueHandle(), reinterpret_cast(&radioTxPower), sizeof(int8_t));
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_SUCCESS;
+}
+
+void EddystoneService::readAdvTxPowerAuthorizationCallback(GattReadAuthCallbackParams *authParams)
+{
+ LOG(("\r\nDO READ ADV TXPOWER slot=%d\r\n", activeSlot));
+ if (lockState == LOCKED) {
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_ATTERR_READ_NOT_PERMITTED;
+ return;
+ }
+ int8_t advTxPower = slotAdvTxPowerLevels[activeSlot];
+ ble.gattServer().write(advTxPowerChar->getValueHandle(), reinterpret_cast(&advTxPower), sizeof(int8_t));
+ authParams->authorizationReply = AUTH_CALLBACK_REPLY_SUCCESS;
+}
+
+/*
+ * This callback is invoked when a GATT client attempts to modify any of the
+ * characteristics of this service. Attempts to do so are also applied to
+ * the internal state of this service object.
+ */
+void EddystoneService::onDataWrittenCallback(const GattWriteCallbackParams *writeParams)
+{
+ uint16_t handle = writeParams->handle;
+ LOG(("\r\nDO WRITE: Handle=%d Len=%d\r\n", handle, writeParams->len));
+ // CHAR-1 CAPABILITIES
+ /* capabilitySlotChar is READ ONLY */
+ // CHAR-2 ACTIVE SLOT
+ if (handle == activeSlotChar->getValueHandle()) {
+ LOG(("Write: Active Slot Handle=%d\r\n", handle));
+ uint8_t slot = *(writeParams->data);
+ LOG(("Active Slot=%d\r\n", slot));
+ // Ensure slot does not exceed limit, or set highest slot
+ if (slot < MAX_ADV_SLOTS) {
+ activeSlot = slot;
+ }
+ ble.gattServer().write(activeSlotChar->getValueHandle(), &activeSlot, sizeof(uint8_t));
+ // CHAR-3 ADV INTERVAL
+ } else if (handle == advIntervalChar->getValueHandle()) {
+ LOG(("Write: Interval Handle=%d\r\n", handle));
+ uint16_t interval = correctAdvertisementPeriod(swapEndian(*((uint16_t *)(writeParams->data))));
+ slotAdvIntervals[activeSlot] = interval; // Store this value for reading
+ uint16_t beAdvInterval = swapEndian(slotAdvIntervals[activeSlot]);
+ ble.gattServer().write(advIntervalChar->getValueHandle(), reinterpret_cast(&beAdvInterval), sizeof(uint16_t));
+ // CHAR-4 RADIO TX POWER
+ } else if (handle == radioTxPowerChar->getValueHandle()) {
+ LOG(("Write: RADIO Power Handle=%d\r\n", handle));
+ int8_t radioTxPower = *(writeParams->data);
+ uint8_t index = radioTxPowerToIndex(radioTxPower);
+ radioTxPower = radioTxPowerLevels[index]; // Power now corrected to nearest allowed power
+ slotRadioTxPowerLevels[activeSlot] = radioTxPower; // Store by slot number
+ int8_t advTxPower = advTxPowerLevels[index]; // Determine adv power equivalent
+ slotAdvTxPowerLevels[activeSlot] = advTxPower;
+ setFrameTxPower(activeSlot, advTxPower); // Set the actual frame radio TxPower for this slot
+ ble.gattServer().write(radioTxPowerChar->getValueHandle(), reinterpret_cast(&radioTxPower), sizeof(int8_t));
+ // CHAR-5 ADV TX POWER
+ } else if (handle == advTxPowerChar->getValueHandle()) {
+ LOG(("Write: ADV Power Handle=%d\r\n", handle));
+ int8_t advTxPower = *(writeParams->data);
+ slotAdvTxPowerLevels[activeSlot] = advTxPower;
+ setFrameTxPower(activeSlot, advTxPower); // Update the actual frame Adv TxPower for this slot
+ ble.gattServer().write(advTxPowerChar->getValueHandle(), reinterpret_cast(&advTxPower), sizeof(int8_t));
+ // CHAR-6 LOCK STATE
+ } else if (handle == lockStateChar->getValueHandle()) {
+ LOG(("Write: Lock State Handle=%d\r\n", handle));
+ uint8_t newLockState = *(writeParams->data);
+ if ((writeParams->len == sizeof(uint8_t)) || (writeParams->len == sizeof(uint8_t) + sizeof(Lock_t))) {
+ if ((newLockState == LOCKED) || (newLockState == UNLOCKED) || (newLockState == UNLOCKED_AUTO_RELOCK_DISABLED)) {
+ lockState = newLockState;
+ }
+ }
+ if ((newLockState == LOCKED) && (writeParams->len == (sizeof(uint8_t) + sizeof(Lock_t))) ) {
+ // And sets the new secret lock code if present
+ uint8_t encryptedNewKey[sizeof(Lock_t)];
+ uint8_t newKey[sizeof(Lock_t)];
+ memcpy(encryptedNewKey, (writeParams->data)+1, sizeof(Lock_t));
+ // Decrypt the new key
+ aes128Decrypt(unlockKey, encryptedNewKey, newKey);
+ memcpy(unlockKey, newKey, sizeof(Lock_t));
+ }
+ ble.gattServer().write(lockStateChar->getValueHandle(), reinterpret_cast(&lockState), sizeof(uint8_t));
+ // CHAR-7 UNLOCK
+ } else if (handle == unlockChar->getValueHandle()) {
+ LOG(("Write: Unlock Handle=%d\r\n", handle));
+ // NOTE: Actual comparison with unlock code is done in:
+ // writeUnlockAuthorizationCallback(...) which is executed before this method call.
+ lockState = UNLOCKED;
+ // Regenerate challenge and expected unlockToken for Next unlock operation
+ generateRandom(challenge, sizeof(Lock_t));
+ aes128Encrypt(unlockKey, challenge, unlockToken);
+ // Update Chars
+ ble.gattServer().write(unlockChar->getValueHandle(), reinterpret_cast(challenge), sizeof(Lock_t)); // Update the challenge
+ ble.gattServer().write(lockStateChar->getValueHandle(), reinterpret_cast(&lockState), sizeof(uint8_t)); // Update the lock
+ // CHAR-8 PUBLIC ECDH KEY
+ /* PublicEchdChar is READ ONLY */
+ // CHAR-9 EID INDENTITY KEY
+ /* EidIdentityChar is READ ONLY */
+ // CHAR-10 ADV DATA
+ } else if (handle == advSlotDataChar->getValueHandle()) {
+ LOG(("Write: Adv Slot DATA Handle=%d\r\n", handle));
+ uint8_t* frame = slotToFrame(activeSlot);
+ int8_t advTxPower = slotAdvTxPowerLevels[activeSlot];
+ uint8_t writeFrameFormat = *(writeParams->data);
+ uint8_t writeFrameLen = (writeParams->len);
+ uint8_t writeData[34];
+ uint8_t serverPublicEcdhKey[32];
+
+ if (writeFrameLen != 0) {
+ writeFrameLen--; // Remove the Format byte from the count
+ } else {
+ writeFrameFormat = UNDEFINED_FRAME_FORMAT; // Undefined format
+ }
+
+ memcpy(writeData, (writeParams->data) + 1, writeFrameLen);
+ LOG(("ADV Data Write=%d,%d\r\n", writeFrameFormat, writeFrameLen));
+ switch(writeFrameFormat) {
+ case UIDFrame::FRAME_TYPE_UID:
+ if (writeFrameLen == 16) {
+ uidFrame.setData(frame, advTxPower,reinterpret_cast((writeParams->data) + 1));
+ slotFrameTypes[activeSlot] = EDDYSTONE_FRAME_UID;
+ } else if (writeFrameLen == 0) {
+ uidFrame.clearFrame(frame);
+ }
+ break;
+ case URLFrame::FRAME_TYPE_URL:
+ if (writeFrameLen <= 18) {
+ urlFrame.setData(frame, advTxPower, reinterpret_cast((writeParams->data) + 1), writeFrameLen );
+ slotFrameTypes[activeSlot] = EDDYSTONE_FRAME_URL;
+ } else if (writeFrameLen == 0) {
+ urlFrame.clearFrame(frame);
+ }
+ break;
+ case TLMFrame::FRAME_TYPE_TLM:
+ if (writeFrameLen == 0) {
+ updateRawTLMFrame(frame);
+ tlmFrame.setData(frame);
+ int slot = getEidSlot();
+ LOG(("WRITE: Testing if TLM or ETLM=%d\r\n", slot));
+ if (slot != NO_EID_SLOT_SET) {
+ LOG(("WRITE: Configuring ETLM Slot time(S)=%lu\r\n", getTimeSinceFirstBootSecs() ));
+ tlmFrame.encryptData(frame, slotEidIdentityKeys[slot], slotEidRotationPeriodExps[slot], getTimeSinceFirstBootSecs() );
+ }
+ slotFrameTypes[activeSlot] = EDDYSTONE_FRAME_TLM;
+ }
+ break;
+ case EIDFrame::FRAME_TYPE_EID:
+ LOG(("EID Len=%d\r\n", writeFrameLen));
+ if (writeFrameLen == 17) {
+ // Least secure
+ LOG(("EID Insecure branch\r\n"));
+ aes128Decrypt(unlockKey, writeData, slotEidIdentityKeys[activeSlot]);
+ slotEidRotationPeriodExps[activeSlot] = writeData[16]; // index 16 is the exponent
+ ble.gattServer().write(eidIdentityKeyChar->getValueHandle(), reinterpret_cast(&writeData), sizeof(EidIdentityKey_t));
+ } else if (writeFrameLen == 33 ) {
+ // Most secure
+ memcpy(serverPublicEcdhKey, writeData, 32);
+ ble.gattServer().write(publicEcdhKeyChar->getValueHandle(), reinterpret_cast(&serverPublicEcdhKey), sizeof(PublicEcdhKey_t));
+ LOG(("ServerPublicEcdhKey=")); logPrintHex(serverPublicEcdhKey, 32);
+ slotEidRotationPeriodExps[activeSlot] = writeData[32]; // index 32 is the exponent
+ LOG(("Exponent=%i\r\n", writeData[32]));
+ LOG(("genBeaconKeyRC=%x\r\n", genBeaconKeyRC));
+ LOG(("BeaconPrivateEcdhKey=")); logPrintHex(privateEcdhKey, 32);
+ LOG(("BeaconPublicEcdhKey=")); logPrintHex(publicEcdhKey, 32);
+ LOG(("genECDHShareKey\r\n"));
+ int rc = eidFrame.genEcdhSharedKey(privateEcdhKey, publicEcdhKey, serverPublicEcdhKey, slotEidIdentityKeys[activeSlot]);
+ LOG(("Gen Keys RC = %x\r\n", rc));
+ LOG(("Generated eidIdentityKey=")); logPrintHex(slotEidIdentityKeys[activeSlot], 16);
+ aes128Encrypt(unlockKey, slotEidIdentityKeys[activeSlot], encryptedEidIdentityKey);
+ LOG(("encryptedEidIdentityKey=")); logPrintHex(encryptedEidIdentityKey, 16);
+ ble.gattServer().write(eidIdentityKeyChar->getValueHandle(), reinterpret_cast(&encryptedEidIdentityKey), sizeof(EidIdentityKey_t));
+ } else if (writeFrameLen == 0) {
+ // Reset eidFrame
+ eidFrame.clearFrame(frame);
+ break;
+ } else {
+ break; // Do nothing, this is not a recognized Frame length
+ }
+ // Establish the new frame type
+ slotFrameTypes[activeSlot] = EDDYSTONE_FRAME_EID;
+ nextEidSlot = activeSlot; // This was the last one updated
+ LOG(("update Eid Frame\r\n"));
+ // Generate EID ADV frame packet
+ eidFrame.setData(frame, advTxPower, nullEid);
+ // Fill in the correct EID Value from the Identity Key/exp/clock
+ eidFrame.update(frame, slotEidIdentityKeys[activeSlot], slotEidRotationPeriodExps[activeSlot], getTimeSinceFirstBootSecs() );
+ LOG(("END update Eid Frame\r\n"));
+ break;
+ default:
+ frame[0] = 0; // Frame format unknown so clear the entire frame by writing 0 to its length
+ break;
+ }
+ // Read takes care of setting the Characteristic Value
+ // CHAR-11 FACTORY RESET
+ } else if (handle == factoryResetChar->getValueHandle() && (*((uint8_t *)writeParams->data) != 0)) {
+ LOG(("Write: Factory Reset: Handle=%d\r\n", handle));
+ // Reset params to default values
+ doFactoryReset();
+ // Update all characteristics based on params
+ updateCharacteristicValues();
+ // CHAR-12 REMAIN CONNECTABLE
+ } else if (handle == remainConnectableChar->getValueHandle()) {
+ LOG(("Write: Remain Connectable Handle=%d\r\n", handle));
+ remainConnectable = *(writeParams->data);
+ ble.gattServer().write(remainConnectableChar->getValueHandle(), &remainConnectable, sizeof(uint8_t));
+ }
+
+}
+
+void EddystoneService::setFrameTxPower(uint8_t slot, int8_t advTxPower) {
+ uint8_t* frame = slotToFrame(slot);
+ uint8_t frameType = slotFrameTypes[slot] << 4; // Converting the enum to an actual frame type
+ switch (frameType) {
+ case UIDFrame::FRAME_TYPE_UID:
+ uidFrame.setAdvTxPower(frame, advTxPower);
+ break;
+ case URLFrame::FRAME_TYPE_URL:
+ urlFrame.setAdvTxPower(frame, advTxPower);
+ break;
+ case EIDFrame::FRAME_TYPE_EID:
+ eidFrame.setAdvTxPower(frame, advTxPower);
+ break;
+ }
+}
+
+uint8_t EddystoneService::radioTxPowerToIndex(int8_t txPower) {
+ // NOTE: txPower is an 8-bit signed number
+ uint8_t size = sizeof(PowerLevels_t);
+ // Look for the value in range (or next biggest value)
+ for (uint8_t i = 0; i < size; i++) {
+ if (txPower <= radioTxPowerLevels[i]) {
+ return i;
+ }
+ }
+ return size - 1;
+}
+
+/** AES128 encrypts a 16-byte input array with a key, resulting in a 16-byte output array */
+void EddystoneService::aes128Encrypt(uint8_t key[], uint8_t input[], uint8_t output[]) {
+ mbedtls_aes_context ctx;
+ mbedtls_aes_init(&ctx);
+ mbedtls_aes_setkey_enc(&ctx, key, 8 * sizeof(Lock_t));
+ mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, input, output);
+ mbedtls_aes_free(&ctx);
+}
+
+/** AES128 decrypts a 16-byte input array with a key, resulting in a 16-byte output array */
+void EddystoneService::aes128Decrypt(uint8_t key[], uint8_t input[], uint8_t output[]) {
+ mbedtls_aes_context ctx;
+ mbedtls_aes_init(&ctx);
+ mbedtls_aes_setkey_dec(&ctx, key, 8 * sizeof(Lock_t));
+ mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_DECRYPT, input, output);
+ mbedtls_aes_free(&ctx);
+}
+
+
+
+#ifdef HARDWARE_RANDOM_NUM_GENERATOR
+// Generates a set of random values in byte array[size] based on hardware source
+void EddystoneService::generateRandom(uint8_t ain[], int size) {
+ mbedtls_entropy_context entropy;
+ mbedtls_entropy_init(&entropy);
+ // init entropy source
+ eddystoneRegisterEntropySource(&entropy);
+ mbedtls_ctr_drbg_context ctr_drbg;
+ mbedtls_ctr_drbg_init(&ctr_drbg);
+ mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, NULL, 0);
+ mbedtls_ctr_drbg_random(&ctr_drbg, ain, size);
+ mbedtls_ctr_drbg_free(&ctr_drbg);
+ mbedtls_entropy_free(&entropy);
+ return;
+}
+#else
+// Generates a set of random values in byte array[size] seeded by the clock(ms)
+void EddystoneService::generateRandom(uint8_t ain[], int size) {
+ int i;
+ // Random seed based on boot time in milliseconds
+ srand(getTimeSinceLastBootMs());
+ for (i = 0; i < size; i++) {
+ ain[i] = rand() % 256;
+ }
+ return;
+}
+#endif
+
+/** Reverse Even sized Array endianess: Big to Little or Little to Big */
+void EddystoneService::swapEndianArray(uint8_t ptrIn[], uint8_t ptrOut[], int size) {
+ int i;
+ for (i = 0; i < size; i++) {
+ ptrOut[i] = ptrIn[size - i - 1];
+ }
+ return;
+}
+
+/** Reverse endianess: Big to Little or Little to Big */
+uint16_t EddystoneService::swapEndian(uint16_t arg) {
+ return (arg / 256) + (arg % 256) * 256;
+}
+
+uint16_t EddystoneService::correctAdvertisementPeriod(uint16_t beaconPeriodIn) const
+{
+ /* Re-map beaconPeriod to within permissible bounds if necessary. */
+ if (beaconPeriodIn != 0) {
+ if (beaconPeriodIn < ble.gap().getMinNonConnectableAdvertisingInterval()) {
+ return ble.gap().getMinNonConnectableAdvertisingInterval();
+ } else if (beaconPeriodIn > ble.gap().getMaxAdvertisingInterval()) {
+ return ble.gap().getMaxAdvertisingInterval();
+ }
+ }
+ return beaconPeriodIn;
+}
+
+void EddystoneService::logPrintHex(uint8_t* a, int len) {
+ for (int i = 0; i < len; i++) {
+ LOG(("%x%x", a[i] >> 4, a[i] & 0x0f ));
+ }
+ LOG(("\r\n"));
+}
+
+void EddystoneService::setRandomMacAddress(void) {
+#ifdef EID_RANDOM_MAC
+ uint8_t macAddress[6]; // 48 bit Mac Address
+ generateRandom(macAddress, 6);
+ macAddress[5] |= 0xc0; // Ensure upper two bits are 11's for Random Add
+ ble.setAddress(BLEProtocol::AddressType::RANDOM_STATIC, macAddress);
+#endif
+}
+
+int EddystoneService::getEidSlot(void) {
+ int eidSlot = NO_EID_SLOT_SET; // by default;
+ for (int i = 0; i < MAX_ADV_SLOTS; i++) {
+ if (slotFrameTypes[nextEidSlot] == EDDYSTONE_FRAME_EID) {
+ eidSlot = nextEidSlot;
+ nextEidSlot = (nextEidSlot-1) % MAX_ADV_SLOTS;
+ break;
+ }
+ nextEidSlot = (nextEidSlot-1) % MAX_ADV_SLOTS; // ensure the slot numbers wrap
+ }
+ return eidSlot;
+}
+
+bool EddystoneService::isLocked(void) {
+ if (lockState == LOCKED) {
+ return true;
+ } else {
+ return false;
+ }
+}
+
+/**
+ * Time : Stable Storage
+ */
+
+/**
+ * Returns the time since FIRST Boot (Time in Prior Boots + Time since Last Boot) in SECONDS
+ */
+uint32_t EddystoneService::getTimeSinceFirstBootSecs(void) {
+ timeParams.timeSinceLastBoot = getTimeSinceLastBootMs() / 1000;
+ uint32_t totalTimeSinceFirstBoot = timeParams.timeSinceLastBoot + timeParams.timeInPriorBoots;
+ // Timer Overflow condition = 136 years (32 bits in seconds) so no need for wrap check
+ return totalTimeSinceFirstBoot;
+}
+
+/**
+ * Returns the time since last boot in MILLISECONDS
+ * NOTE: This solution is needed as a stopgap until the Timer API is updated to 64-bit
+ */
+uint64_t EddystoneService::getTimeSinceLastBootMs(void) {
+ static uint64_t time64bit = 0;
+ time64bit += timeSinceBootTimer.read_ms();
+ timeSinceBootTimer.reset();
+ return time64bit;
+}
+
+/**
+ * Store only the time params in Pstorage(e.g. NVM), to maintain time between boots
+ * NOTE: Platform-specific implementation for persistence on the nRF5x. Based on the
+ * pstorage module provided by the Nordic SDK.
+ */
+void EddystoneService::nvmSaveTimeParams(void) {
+ LOG(("Time NVM: "));
+ LOG(("PriorBoots=%lu, SinceBoot=%lu\r\n", timeParams.timeInPriorBoots, timeParams.timeSinceLastBoot));
+ saveEddystoneTimeParams(&timeParams);
+}
+
+/*
+ * Establish constant arrays
+ */
+const uint8_t EddystoneService::slotDefaultUids[MAX_ADV_SLOTS][16] = EDDYSTONE_DEFAULT_SLOT_UIDS;
+
+const uint8_t EddystoneService::slotDefaultEidIdentityKeys[MAX_ADV_SLOTS][16] = EDDYSTONE_DEFAULT_SLOT_EID_IDENTITY_KEYS;
+
+const uint8_t EddystoneService::nullEid[8] = {0,0,0,0,0,0,0,0};
+
+
+
diff --git a/implementations/mbed/source/EddystoneService.h b/implementations/mbed/source/EddystoneService.h
new file mode 100644
index 0000000..a0bbebf
--- /dev/null
+++ b/implementations/mbed/source/EddystoneService.h
@@ -0,0 +1,1198 @@
+/*
+ * Copyright (c) 2006-2016 Google Inc, All Rights Reserved
+ *
+ * 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.
+ */
+
+#ifndef __EDDYSTONESERVICE_H__
+#define __EDDYSTONESERVICE_H__
+//
+// 2016-03 Eddystone Unified GATT
+//
+#include "EventQueue/EventQueue.h"
+#include "ble/BLE.h"
+#include "EddystoneTypes.h"
+#include "UIDFrame.h"
+#include "URLFrame.h"
+#include "TLMFrame.h"
+#include "EIDFrame.h"
+#include
+#include "mbedtls/aes.h"
+#include "mbedtls/entropy.h"
+#include "mbedtls/ctr_drbg.h"
+// #include "PersistentStorageHelper/ConfigParamsPersistence.h"
+
+#ifdef YOTTA_CFG_MBED_OS
+ #include "mbed-drivers/mbed.h"
+ #include "mbed-drivers/CircularBuffer.h"
+#else
+ #include "mbed.h"
+ #include "CircularBuffer.h"
+#endif
+
+#include "stdio.h"
+#include "Eddystone_config.h"
+#include "pstorage_platform.h"
+
+/**
+ * This class implements the Eddystone-URL Config Service and the Eddystone
+ * Protocol Specification as defined in the publicly available specification at
+ * https://github.com/google/eddystone/blob/master/protocol-specification.md.
+ */
+class EddystoneService
+{
+public:
+ /**
+ * Total number of GATT Characteristics in the Eddystonei-URL Configuration
+ * Service.
+ */
+ static const uint16_t TOTAL_CHARACTERISTICS = 12;
+
+ /**
+ * Max data that can be written to the data characteristic
+ */
+ static const uint8_t MAX_DATA_WRITE = 34; // FrameType+32B(IdentityKey)+Exp
+
+ /**
+ * Default interval for advertising packets for the Eddystone-URL
+ * Configuration Service.
+ */
+ static const uint32_t DEFAULT_CONFIG_PERIOD_MSEC = EDDYSTONE_DEFAULT_CONFIG_ADV_INTERVAL;
+
+ /**
+ * Enumeration that defines the various operation modes of the
+ * EddystoneService.
+ *
+ * @note The main app can change the mode of EddystoneService at any point
+ * of time by calling startConfigService() or startBeaconService().
+ * Resources from the previous mode will be freed.
+ *
+ * @note It is currently NOT possible to force EddystoneService back into
+ * EDDYSTONE_MODE_NONE.
+ */
+ enum OperationModes {
+ /**
+ * NONE: EddystoneService has been initialized but no memory has been
+ * dynamically allocated. Additionally, no services are running
+ * nothing is being advertised.
+ */
+ EDDYSTONE_MODE_NONE,
+ /**
+ * CONFIG: EddystoneService has been initialized, the configuration
+ * service started and memory has been allocated for BLE
+ * characteristics. Memory consumption peaks during CONFIG
+ * mode.
+ */
+ EDDYSTONE_MODE_CONFIG,
+ /**
+ * BEACON: Eddystone service is running as a beacon advertising URL,
+ * UID and/or TLM frames depending on how it is configured.
+ */
+ EDDYSTONE_MODE_BEACON
+ };
+
+ /**
+ * Structure that encapsulates the Eddystone configuration parameters. This
+ * structure is particularly useful when storing the parameters to
+ * persistent storage.
+ */
+ struct EddystoneParams_t {
+ /**
+ *
+ */
+ TimeParams_t timeParams;
+ /**
+ * A buffer describing the capabilities of the beacon
+ */
+ Capability_t capabilities;
+
+ /**
+ * Defines the slot that advInterval, radioPower, advPower, advSlotData operate on
+ */
+ uint8_t activeSlot;
+
+ /**
+ * The Beacon interval for each beacon slot
+ *
+ * @note A value of zero disables Eddystone-URL frame trasmissions.
+ */
+ SlotAdvIntervals_t slotAdvIntervals;
+
+ /**
+ * The Radio TX Powers supported by this beacon
+ */
+ PowerLevels_t radioTxPowerLevels;
+
+ /**
+ * The Radio TX Power set for each slot
+ */
+ SlotTxPowerLevels_t slotRadioTxPowerLevels;
+
+ /**
+ * The Calibrated Adv TX Powers supported by this beacon (one for each radio power)
+ */
+ PowerLevels_t advTxPowerLevels;
+
+ /**
+ * The Adv TX Power set for each slot
+ */
+ SlotTxPowerLevels_t slotAdvTxPowerLevels;
+
+ /**
+ * The value of the Eddystone-URL Configuration Service Lock State
+ * characteristic.
+ */
+ uint8_t lockState;
+
+ /**
+ * The value of the Eddystone-URL Configuration Service Unlock
+ * characteristic that can be used to unlock the beacon and clear the
+ * single-use lock-code.
+ */
+ Lock_t unlockToken;
+
+ /**
+ * An array holding the 128-bit unlockKey (big endian)
+ */
+ Lock_t unlockKey;
+
+ /**
+ * An array holding the 128-bit challenge (big endian) in the
+ * challenge/response unlock protocol
+ */
+ Lock_t challenge;
+
+ /**
+ * EID: An array holding the slot rotation period exponents
+ */
+ SlotEidRotationPeriodExps_t slotEidRotationPeriodExps;
+
+ /**
+ * EID: An array holding the slot 128-bit EID Identity Key (big endian)
+ */
+ SlotEidIdentityKeys_t slotEidIdentityKeys;
+
+ /**
+ * Specifies the type of each frame indexed by slot
+ */
+ SlotFrameTypes_t slotFrameTypes;
+
+ /**
+ * A buffer that contains all slot frames, 32-bytes allocated to each frame
+ */
+ SlotStorage_t slotStorage;
+
+ /**
+ * The state of the recently invoked Factory Reset characteristic
+ */
+ uint8_t factoryReset;
+
+ /**
+ * The state of the recently invoked Remain Connectable characteristic
+ */
+ uint8_t remainConnectable;
+ };
+
+ /**
+ * Enumeration that defines the various error codes for EddystoneService.
+ */
+ enum EddystoneError_t {
+ /**
+ * No error occurred.
+ */
+ EDDYSTONE_ERROR_NONE,
+ /**
+ * The supplied advertising interval is invalid. The interval may be
+ * too short/long for the type of advertising packets being broadcast.
+ *
+ * @note For the acceptable range of advertising interval refer to the
+ * following functions in mbed BLE API:
+ * - Gap::getMinNonConnectableAdvertisingInterval()
+ * - Gap::getMinAdvertisingInterval()
+ * - Gap::getMaxAdvertisingInterval()
+ */
+ EDDYSTONE_ERROR_INVALID_ADVERTISING_INTERVAL,
+ /**
+ * The result of executing a call when the the EddystoneService is in
+ * the incorrect operation mode.
+ */
+ EDDYSTONE_ERROR_INVALID_STATE
+ };
+
+ /**
+ * Enumeration that defines the available frame types within Eddystone
+ * advertising packets.
+ */
+ enum FrameType {
+ /**
+ * The Eddystone-UID frame. Refer to
+ * https://github.com/google/eddystone/tree/master/eddystone-uid.
+ */
+ EDDYSTONE_FRAME_UID,
+ /**
+ * The Eddystone-URL frame. Refer to
+ * https://github.com/google/eddystone/tree/master/eddystone-url.
+ */
+ EDDYSTONE_FRAME_URL,
+ /**
+ * The Eddystone-TLM frame. Refer to
+ * https://github.com/google/eddystone/tree/master/eddystone-tlm.
+ */
+ EDDYSTONE_FRAME_TLM,
+ /**
+ * The Eddystone-EID frame. Refer to
+ * https://github.com/google/eddystone/tree/master/eddystone-eid.
+ */
+ EDDYSTONE_FRAME_EID,
+ /**
+ * The total number Eddystone frame types.
+ */
+ NUM_EDDYSTONE_FRAMES
+ };
+
+ typedef eq::EventQueue event_queue_t;
+
+ /**
+ * Constructor that Initializes the EddystoneService using parameters from
+ * the supplied EddystoneParams_t. This constructor is particularly useful
+ * for configuring the EddystoneService with parameters fetched from
+ * persistent storage.
+ *
+ * @param[in] bleIn
+ * The BLE instance.
+ * @param[in] paramIn
+ * The input Eddystone configuration parameters.
+ * @param[in] radioPowerLevelsIn
+ * The value set internally into the radion tx power.
+ * @param[in] eventQueue
+ * The event queue used by the service to schedule tasks.
+ * @param[in] advConfigIntervalIn
+ * The advertising interval for advertising packets of the
+ * Eddystone-URL Configuration Service.
+ */
+ EddystoneService(BLE &bleIn,
+ EddystoneParams_t ¶msIn,
+ const PowerLevels_t &radioPowerLevelsIn,
+ event_queue_t &eventQueue,
+ uint32_t advConfigIntervalIn = DEFAULT_CONFIG_PERIOD_MSEC);
+
+ /**
+ * Constructor to initialize the EddystoneService to default values.
+ *
+ * @param[in] bleIn
+ * The BLE instance.
+ * @param[in] advPowerLevelsIn
+ * The value of the Eddystone-URL Configuration Service TX
+ * Power Mode characteristic.
+ * @param[in] radioPowerLevelsIn
+ * The value set internally into the radion tx power.
+ * @param[in] eventQueue
+ * The event queue used by the service to schedule tasks.
+ * @param[in] advConfigIntervalIn
+ * The advertising interval for advertising packets of the
+ * Eddystone-URL Configuration Service.
+ *
+ * @note When using this constructor the setURLData(), setTMLData() and
+ * setUIDData() and setEIDData() functions must be called to initialize
+ * EddystoneService manually.
+ */
+ EddystoneService(BLE &bleIn,
+ const PowerLevels_t &advPowerLevelsIn,
+ const PowerLevels_t &radioPowerLevelsIn,
+ event_queue_t &eventQueue,
+ uint32_t advConfigIntervalIn = DEFAULT_CONFIG_PERIOD_MSEC);
+
+
+ /**
+ * Generate the EID Beacon Random ECHD Keys (private and Public)
+ */
+ void genEIDBeaconKeys(void);
+
+ /**
+ * Factory Reset all parameters in the beacon
+ */
+ void doFactoryReset(void);
+
+ /**
+ * Setup callback to update BatteryVoltage in Eddystone-TLM frames
+ *
+ * @param[in] tlmBatteryVoltageCallbackIn
+ * The callback being registered.
+ */
+ void onTLMBatteryVoltageUpdate(TlmUpdateCallback_t tlmBatteryVoltageCallbackIn);
+
+ /**
+ * Setup callback to update BeaconTemperature in Eddystone-TLM frames
+ *
+ * @param[in] tlmBeaconTemperatureCallbackIn
+ * The callback being registered.
+ */
+ void onTLMBeaconTemperatureUpdate(TlmUpdateCallback_t tlmBeaconTemperatureCallbackIn);
+
+ /**
+ * Change the EddystoneService OperationMode to EDDYSTONE_MODE_CONFIG.
+ *
+ * @retval EDDYSTONE_ERROR_NONE if the operation succeeded.
+ * @retval EDDYSONE_ERROR_INVALID_ADVERTISING_INTERVAL if the configured
+ * advertising interval is zero.
+ *
+ * @note If EddystoneService was previously in EDDYSTONE_MODE_BEACON, then
+ * the resources allocated to that mode of operation such as memory
+ * are freed and the BLE instance shutdown before the new operation
+ * mode is configured.
+ */
+ EddystoneError_t startConfigService(void);
+
+ /**
+ * Change the EddystoneService to start transmitting Eddystone beacons
+ * operationMode = EDDYSTONE_MODE_BEACON
+ *
+ * @retval EDDYSTONE_ERROR_NONE if the operation succeeded.
+ * @retval EDDYSONE_ERROR_INVALID_ADVERTISING_INTERVAL if the configured
+ * advertising interval is zero.
+ *
+ * @note If EddystoneService was previously in EDDYSTONE_MODE_CONFIG, then
+ * the resources allocated to that mode of operation such as memory
+ * are freed and the BLE instance shutdown before the new operation
+ * mode is configured.
+ */
+ EddystoneError_t startEddystoneBeaconAdvertisements(void);
+
+ /**
+ * Set the Comple Local Name for the BLE device. This not only updates
+ * the value of the Device Name Characteristic, it also updates the scan
+ * response payload if the EddystoneService is currently in
+ * EDDYSTONE_MODE_CONFIG.
+ *
+ * @param[in] deviceNameIn
+ * A pointer to a null terminated string containing the new
+ * device name.
+ *
+ * @return BLE_ERROR_NONE if the name was successfully set. Otherwise an
+ * appropriate error.
+ *
+ * @note EddystoneService does not make an internal copy of the string
+ * pointed to by @p deviceNameIn. Therefore, the user is responsible
+ * for ensuring that the string persists in memory as long as it is
+ * in use by the EddystoneService.
+ *
+ * @note The device name is not considered an Eddystone configuration
+ * parameter; therefore, it is not contained within the
+ * EddystoneParams_t structure and must be stored to persistent
+ * storage separately.
+ */
+ ble_error_t setCompleteDeviceName(const char *deviceNameIn);
+
+ /**
+ * Get the Eddystone Configuration parameters. This is particularly useful
+ * for storing the configuration parameters in persistent storage.
+ * It is not the responsibility of the Eddystone implementation to store
+ * the configured parameters in persistent storage since this is
+ * platform-specific.
+ *
+ * @param[out] params
+ * A reference to an EddystoneParams_t structure with the
+ * configured parameters of the EddystoneService.
+ */
+ void getEddystoneParams(EddystoneParams_t ¶ms);
+
+ /**
+ * Start advertising packets indicating the Eddystone Configuration state
+ * operationMode = EDDYSTONE_MODE_CONFIG
+ */
+ EddystoneService::EddystoneError_t startEddystoneConfigAdvertisements(void);
+
+ /**
+ * Free the resources acquired by a call to setupBeaconService() and
+ * cancel all pending callbacks that operate the radio and frame queue.
+ *
+ * @note This call will not modify the current state of the BLE device.
+ * EddystoneService::stopBeaconService should only be called after
+ * a call to BLE::shutdown().
+ */
+ void stopEddystoneBeaconAdvertisements(void);
+
+ /**
+ * Initialize and start the BLE Eddystone Configuration Service
+ * This will create the 12-characteristics of the service and make them
+ * available when a client connects
+ */
+ void startEddystoneConfigService();
+
+ /**
+ * Stops the Eddystone Configuration Service and frees its resources
+ * and cancels all pending callbacks that operate the radio and frame queue.
+ *
+ * @note This call will not modify the current state of the BLE device.
+ * EddystoneService::stopBeaconService should only be called after
+ * a call to BLE::shutdown().
+ */
+ void stopEddystoneConfigService();
+
+ /**
+ * Tests if the beacon is locked or not
+ *
+ * @return bool
+ */
+ bool isLocked();
+
+ /**
+ * Print an array as a set of hex values
+ *
+ * @param[in] a
+ * The array to be printed.
+ *
+ * @param[in] len
+ * The length of the array.
+ *
+ * @return void
+ *
+ */
+ static void logPrintHex(uint8_t* a, int len);
+
+ /**
+ * Swaps the endianess of an array ptrIn[size] to ptrOut[size]
+ *
+ * @param[in] *ptrIn
+ * The input array
+ * @param[in] *ptrOut
+ * The output array
+ * @param[in] size
+ * The sizes of the arrays (num bytes to be reversed)
+ */
+ static void swapEndianArray(uint8_t *ptrIn, uint8_t *ptrOut, int size);
+
+ /**
+ * Generate a random array of bytes of length size
+ *
+ * @param[in] *ain
+ * The input/output array
+ * @param[in] size
+ * The size of the array in bytes
+ */
+ static void generateRandom(uint8_t *ain, int size);
+
+ /**
+ * Timer that keeps track of the time since boot.
+ */
+ static Timer timeSinceBootTimer;
+
+private:
+
+ static const uint8_t NO_EID_SLOT_SET = 0xff;
+
+ static const uint8_t UNDEFINED_FRAME_FORMAT = 0xff;
+
+ static const uint8_t REMAIN_CONNECTABLE_SET = 0x01;
+
+ static const uint8_t REMAIN_CONNECTABLE_UNSET = 0x00;
+
+ static const uint8_t CONFIG_FRAME_HDR_LEN = 4;
+
+ /**
+ * Helper funtion that will be registered as an initialization complete
+ * callback when BLE::shutdown() is called. This is necessary when changing
+ * Eddystone OperationModes. Once the BLE initialization is complete, this
+ * callback will initialize all the necessary resource to operate
+ * Eddystone service in the selected mode.
+ *
+ * @param[in] initContext
+ * The context provided by BLE API when initialization
+ * completes.
+ */
+ void bleInitComplete(BLE::InitializationCompleteCallbackContext* initContext);
+
+ /**
+ * When in EDDYSTONE_MODE_BEACON this function is called to update the
+ * advertising payload to contain the information related to the specified
+ * FrameType.
+ *
+ * @param[in] slot
+ * The slot to populate the advertising payload with.
+ */
+ void swapAdvertisedFrame(int slot);
+
+ /**
+ * Helper function that manages the BLE radio that is used to broadcast
+ * advertising packets. To advertise frames at the configured intervals
+ * the actual advertising interval of the BLE instance is set to the value
+ * returned by Gap::getMaxAdvertisingInterval() from the BLE API. When a
+ * frame needs to be advertised, the enqueueFrame() callbacks add the frame
+ * type to the advFrameQueue and post a manageRadio() callback. When the
+ * callback is executed, the frame is dequeued and advertised using the
+ * radio (by updating the advertising payload). manageRadio() also posts a
+ * callback to itself Gap::getMinNonConnectableAdvertisingInterval()
+ * milliseconds later. In this callback, manageRadio() will advertise the
+ * next frame in the queue, yet if there is none it calls
+ * Gap::stopAdvertising() and does not post any further callbacks.
+ */
+ void manageRadio(void);
+
+ /**
+ * Regular callbacks posted at the rate of slotAdvPeriod[slot] milliseconds
+ * enqueue frames to be advertised. If the
+ * frame queue is currently empty, then this function directly calls
+ * manageRadio() to broadcast the required FrameType.
+ *
+ * @param[in] frameType
+ * The FrameType to enqueue for broadcasting.
+ */
+ void enqueueFrame(int slot);
+
+ /**
+ * Helper function that updates the advertising payload when in
+ * EDDYSTONE_MODE_BEACON to contain a new frame.
+ *
+ * @param[in] rawFrame
+ * The raw bytes of the frame to advertise.
+ * @param[in] rawFrameLength
+ * The length in bytes of the array pointed to by @p rawFrame.
+ */
+ void updateAdvertisementPacket(const uint8_t* rawFrame, size_t rawFrameLength);
+
+ /**
+ * Helper function that updates the information in the Eddystone-TLM frames
+ * Internally, this function executes the registered callbacks to update
+ * beacon Battery Voltage and Temperature (if available). Furthermore, this
+ * function updates the raw frame data. This operation must be done fairly
+ * often because the Eddystone-TLM frame Time Since Boot must have a 0.1
+ * seconds resolution according to the Eddystone specification.
+ */
+ void updateRawTLMFrame(uint8_t* frame);
+
+ /**
+ * Calculate the Frame pointer from the slot number
+ */
+ uint8_t* slotToFrame(int slot);
+
+ /**
+ * Free the characteric resources acquired by a call to
+ * startEddystoneConfigService().
+ */
+ void freeConfigCharacteristics(void);
+
+ /**
+ * Helper function used to update the GATT database following any
+ * change to the internal state of the service object.
+ */
+ void updateCharacteristicValues(void);
+
+ /**
+ * Helper function to setup the payload of scan response packets for
+ * Eddystone-URL Configuration Service.
+ */
+ void setupEddystoneConfigScanResponse(void);
+
+ /**
+ * Callback registered to the BLE API to authorize write operations to the
+ * Eddystone Configuration Service Lock characteristic.
+ *
+ * @param[in] authParams
+ * Write authentication information.
+ */
+ void writeLockAuthorizationCallback(GattWriteAuthCallbackParams *authParams);
+
+ /**
+ * Callback registered to the BLE API to authorize write operations to the
+ * Eddystone Configuration Service Unlock characteristic.
+ *
+ * @param[in] authParams
+ * Write authentication information.
+ */
+ void writeUnlockAuthorizationCallback(GattWriteAuthCallbackParams *authParams);
+
+ /**
+ * Callback registered to the BLE API to authorize write operations to the
+ * Eddystone Configuration Service advSlotData characteristic.
+ *
+ * @param[in] authParams
+ * Write authentication information.
+ */
+ void writeVarLengthDataAuthorizationCallback(GattWriteAuthCallbackParams *authParams);
+
+ /**
+ * Callback registered to the BLE API to authorize write operations to the
+ * lockState characteristic which can be 1 byte or 17 bytes long.
+ *
+ * @param[in] authParams
+ * Write authentication information.
+ */
+ void writeLockStateAuthorizationCallback(GattWriteAuthCallbackParams *authParams);
+
+ /**
+ * Callback registered to the BLE API to authorize write operations to simple fixed length
+ * value characteristic types.
+ *
+ * @param[in] authParams
+ * Write authentication information.
+ */
+ template
+ void writeBasicAuthorizationCallback(GattWriteAuthCallbackParams *authParams);
+
+ /**
+ * This callback is invoked when a GATT client attempts to write to the
+ * Active Slot characteristic of the service.
+ *
+ * @param[in] authParams
+ * Information about the values that are being read.
+ */
+ template
+ void writeActiveSlotAuthorizationCallback(GattWriteAuthCallbackParams *authParams);
+
+ /**
+ * READ AUTHORIZATIONS
+ */
+
+ /**
+ * This callback is invoked when a GATT client attempts to read from a
+ * basic characteristic of the Eddystone Configuration Service, which
+ * is blocked if the beacon lock is set to LOCKED.
+ *
+ * @param[in] authParams
+ * Information about the values that are being read.
+ */
+ void readBasicTestLockAuthorizationCallback(GattReadAuthCallbackParams *authParams);
+
+ /**
+ * This callback is invoked when a GATT client attempts to read from the
+ * EidIdentityKey characteristic of the Eddystone Configuration Service,
+ * which is blocked if the beacon lock is set to LOCKED, or the key has not
+ * been set/initialized.
+ *
+ * @param[in] authParams
+ * Information about the values that are being read.
+ */
+ void readEidIdentityAuthorizationCallback(GattReadAuthCallbackParams *authParams);
+
+ /**
+ * This callback is invoked when a GATT client attempts to read from the
+ * PublicEcdhKey characteristic of the Eddystone Configuration Service,
+ * which is blocked if the beacon lock is set to LOCKED, or the key has not
+ * been set/initialized.
+ *
+ * @param[in] authParams
+ * Information about the values that are being read.
+ */
+ void readPublicEcdhKeyAuthorizationCallback(GattReadAuthCallbackParams *authParams);
+
+
+ /**
+ * This callback is invoked when a GATT client attempts to read from the
+ * Adv Slot Data characteristic of the Eddystone Configuration Service,
+ * which isblocked if the beacon lock is set to LOCKED.
+ *
+ * @param[in] authParams
+ * Information about the values that are being read.
+ */
+ void readDataAuthorizationCallback(GattReadAuthCallbackParams *authParams);
+
+ /**
+ * Checks if this is valid frame data (i.e. length > 0)
+ *
+ * @param[in] frame
+ * The frame being tested
+ * @returns frame is valid or not.
+ */
+ bool testValidFrame(uint8_t* frame);
+
+ /**
+ * This callback is invoked when a GATT client attempts to read the challenge
+ * from the Unlock characteristic of the Eddystone Configuration Service,
+ * which is blocked if the beacon lock is set to UNLOCKED.
+ *
+ * @param[in] authParams
+ * Information about the values that are being read.
+ */
+ void readUnlockAuthorizationCallback(GattReadAuthCallbackParams *authParams);
+
+ /**
+ * This callback is invoked when a GATT client attempts to read from the
+ * Radio Tx Power characteristic of the Eddystone Configuration Service,
+ * which is blocked if the beacon lock is set to LOCKED.
+ *
+ * @param[in] authParams
+ * Information about the values that are being read.
+ */
+ void readRadioTxPowerAuthorizationCallback(GattReadAuthCallbackParams *authParams);
+
+ /**
+ * This callback is invoked when a GATT client attempts to read from the
+ * Radio Tx Power characteristic of the Eddystone Configuration Service,
+ * which is blocked if the beacon lock is set to LOCKED.
+ *
+ * @param[in] authParams
+ * Information about the values that are being read.
+ */
+ void readAdvTxPowerAuthorizationCallback(GattReadAuthCallbackParams *authParams);
+
+ /**
+ * This callback is invoked when a GATT client attempts to read from the
+ * Adv Interval characteristic of the Eddystone Configuration Service,
+ * which is blocked if the beacon lock is set to LOCKED.
+ *
+ * @param[in] authParams
+ * Information about the values that are being read.
+ */
+ void readAdvIntervalAuthorizationCallback(GattReadAuthCallbackParams *authParams);
+
+ /**
+ * Calculates the index in the radio power levels array which can be used
+ * to index into the adv power levels array to find the calibrated adv power
+ * used in the adv frame.
+ */
+ uint8_t radioTxPowerToIndex(int8_t txPower);
+
+ /**
+ * This callback is invoked when a GATT client attempts to modify any of the
+ * characteristics of this service. Attempts to do so are also applied to
+ * the internal state of this service object.
+ *
+ * @param[in] writeParams
+ * Information about the values that are being written.
+ */
+ void onDataWrittenCallback(const GattWriteCallbackParams *writeParams);
+
+ /**
+ * Sets the power for the frame in a particular slot using the
+ * adv tx power parmeter
+ *
+ * @param[in] slot
+ * The the current slot number being considered
+ * @param[in] advTxPower
+ * The adv power required in a frame
+ */
+ void setFrameTxPower(uint8_t slot, int8_t advTxPower);
+
+ /**
+ * AES128 ECB Encrypts a 16-byte input array with a key, to an output array
+ *
+ * @param[in] *key
+ * The encryption key
+ * @param[in] *input
+ * The input array
+ * @param[in] *output
+ * The output array (contains the encrypted data)
+ */
+ void aes128Encrypt(uint8_t *key, uint8_t *input, uint8_t *output);
+
+ /**
+ * AES128 ECB Deccrypts a 16-byte input array with a key, to an output array
+ *
+ * @param[in] *key
+ * The decryption key
+ * @param[in] *input
+ * The input array
+ * @param[in] *output
+ * The output array (containing the decrypted data)
+ */
+ void aes128Decrypt(uint8_t *key, uint8_t *input, uint8_t *output);
+
+
+
+ /**
+ * Swaps the endianess of a 16-bit unsigned int
+ *
+ * @param[in] arg
+ * The value with the byte order to be reversed
+ *
+ * @return The resulting 16-bit value with byte order reversed
+ */
+ uint16_t swapEndian(uint16_t arg);
+
+ /**
+ * Correct the advertising interval for non-connectable packets.
+ *
+ * @param[in] beaconPeriodIn
+ * The input interval in milliseconds.
+ *
+ * @return The corrected interval in milliseconds.
+ *
+ * @note For the acceptable range of advertising interval refer to the
+ * following functions in mbed BLE API:
+ * - Gap::getMinNonConnectableAdvertisingInterval()
+ * - Gap::getMaxAdvertisingInterval()
+ */
+ uint16_t correctAdvertisementPeriod(uint16_t beaconPeriodIn) const;
+
+ /**
+ * Swaps the endianess of a 16-bit unsigned int
+ *
+ * @param[in] arg
+ * The value with the byte order to be reversed
+ *
+ * @return The resulting 16-bit value with byte order reversed
+ */
+ void setRandomMacAddress(void);
+
+ /**
+ * Finds the first EID slot set
+ *
+ * @return slot number (and if not, returns NO_EID_SLOT_SET = -1)
+ */
+ int getEidSlot(void);
+
+ /**
+ * Returns the current time in Secs (Prior Time + Time since boot)
+ *
+ * @return time
+ */
+ uint32_t getTimeSinceFirstBootSecs(void);
+
+
+ /**
+ * Returns the time since boot in Milliseconds
+ *
+ * @return time
+ */
+ static uint64_t getTimeSinceLastBootMs(void);
+
+ /**
+ * Saves only the Time Params in pStorage (a subset of all the Eddsytone Params)
+ * This is more efficient than periodically saving all state (its just 8 bytes)
+ */
+ void nvmSaveTimeParams(void);
+
+ /**
+ * BLE instance that EddystoneService will operate on.
+ */
+ BLE &ble;
+
+ /**
+ * The advertising interval for Eddystone-URL Config Service advertising
+ * packets.
+ */
+ uint32_t advConfigInterval;
+ /**
+ * Current EddystoneServce operation mode.
+ */
+ uint8_t operationMode;
+
+ /**
+ * Parameter to consistently record the return code when generating Beacon Keys
+ */
+ int genBeaconKeyRC;
+
+ /**
+ * Keeps track of time in prior boots and current/last boot
+ */
+ TimeParams_t timeParams;
+
+ /**
+ * GATT Service Variables
+ */
+
+ /**
+ * An array describing the capabilites of the beacon.
+ */
+ Capability_t capabilities;
+
+ /**
+ * The currenty defined active slot.
+ */
+ uint8_t activeSlot;
+
+ /**
+ * An array containing all the adv intervals for each slot index
+ */
+ SlotAdvIntervals_t slotAdvIntervals;
+
+ /**
+ * The value of the Eddystone Configuration Service radioTX Power
+ * characteristic.
+ */
+ SlotTxPowerLevels_t slotRadioTxPowerLevels;
+
+ /**
+ * An array containing the supported radio tx power levels for this beacon
+ */
+ PowerLevels_t radioTxPowerLevels;
+
+ /**
+ * An array containing all possible values for advertised tx power in Eddystone
+ * slots.
+ */
+ SlotTxPowerLevels_t slotAdvTxPowerLevels;
+
+ /**
+ * An array containing the supported adv tx power levels for this beacon
+ */
+ PowerLevels_t advTxPowerLevels;
+
+ /**
+ * The value of the Eddystone Configuration Service Lock State
+ * characteristic.
+ */
+ uint8_t lockState;
+
+
+ /**
+ * The value of the Eddystone Configuration Service Lock State
+ * buffer
+ */
+ LockState_t lockStateBuf;
+
+ /**
+ * The value of the Eddystone Configuration Service unlock key
+ */
+ Lock_t unlockKey;
+
+ /**
+ * The value of the Eddystone Configuration Service unlock challenge
+ */
+ Lock_t challenge;
+
+ /**
+ * The value of the Eddystone Configuration Service unlock token. A write
+ * to the unlock characteristic must contain this token to unlock the beacon
+ */
+ Lock_t unlockToken;
+
+
+ /**
+ * EID: An array holding the 256-bit private Ecdh Key (big endian)
+ */
+ PrivateEcdhKey_t privateEcdhKey;
+
+ /**
+ * EID: An array holding the 256-bit public Ecdh Key (big endian)
+ */
+ PublicEcdhKey_t publicEcdhKey;
+
+ /**
+ * EID: An array holding the 256-bit public Ecdh Key (little endian)
+ */
+ PublicEcdhKey_t publicEcdhKeyLE;
+
+ /**
+ * EID: An array holding the slot rotation period exponents
+ */
+ SlotEidRotationPeriodExps_t slotEidRotationPeriodExps;
+
+ /**
+ * EID: An array holding the slot Eid Identity Keys
+ */
+ SlotEidIdentityKeys_t slotEidIdentityKeys;
+
+ /**
+ * EID: An array holding the slot Eid Public Ecdh Keys
+ */
+ //SlotEidPublicEcdhKeys_t slotEidPublicEcdhKeys;
+
+ /**
+ * Instance of the UID frame.
+ */
+ UIDFrame uidFrame;
+
+ /**
+ * Instance of the URL frame.
+ */
+ URLFrame urlFrame;
+
+ /**
+ * Instance of the TLM frame.
+ */
+ TLMFrame tlmFrame;
+
+ /**
+ * Instance of the EID frame.
+ */
+ EIDFrame eidFrame;
+
+ /**
+ * The value of the Eddystone Configuration Service reset
+ * characteristic.
+ */
+ uint8_t factoryReset;
+
+ /**
+ * The value of the Eddystone Configuration Service Remain Connectable
+ * characteristic.
+ */
+ uint8_t remainConnectable;
+
+ /**
+ * CHARACTERISTIC STORAGE
+ */
+
+ /**
+ * Pointer to the BLE API characteristic encapsulation for the Eddystone
+ * Configuration Service Capabilities characteristic.
+ */
+ ReadOnlyArrayGattCharacteristic *capabilitiesChar;
+
+ /**
+ * Pointer to the BLE API characteristic encapsulation for the Eddystone
+ * Configuration Service Active Slot characteristic.
+ */
+ ReadWriteGattCharacteristic *activeSlotChar;
+
+ /**
+ * Pointer to the BLE API characteristic encapsulation for the Eddystone
+ * Configuration Service Adv Interval characteristic.
+ */
+ ReadWriteGattCharacteristic *advIntervalChar;
+
+ /**
+ * Pointer to the BLE API characteristic encapsulation for the Eddystone
+ * Configuration Service Radio Tx Power characteristic.
+ */
+ ReadWriteGattCharacteristic *radioTxPowerChar;
+
+ /**
+ * Pointer to the BLE API characteristic encapsulation for the Eddystone
+ * Configuration Service Adv Tx Power characteristic.
+ */
+ ReadWriteGattCharacteristic *advTxPowerChar;
+
+ /**
+ * Pointer to the BLE API characteristic encapsulation for the Eddystone
+ * Configuration Service Lock State characteristic.
+ */
+ GattCharacteristic *lockStateChar;
+
+ /**
+ * Pointer to the BLE API characteristic encapsulation for the Eddystone
+ * Configuration Service Unlock characteristic.
+ */
+ ReadWriteArrayGattCharacteristic *unlockChar;
+
+ /**
+ * Pointer to the BLE API characteristic encapsulation for the Eddystone
+ * Configuration Service Public ECDH Key characteristic.
+ */
+ GattCharacteristic *publicEcdhKeyChar;
+
+ /**
+ * Pointer to the BLE API characteristic encapsulation for the Eddystone-URL
+ * Configuration Service EID Identity Key characteristic.
+ */
+ GattCharacteristic *eidIdentityKeyChar;
+
+ /**
+ * Pointer to the BLE API characteristic encapsulation for the Eddystone-URL
+ * Configuration Service Adv Slot Data characteristic.
+ */
+ GattCharacteristic *advSlotDataChar;
+
+ /**
+ * Pointer to the BLE API characteristic encapsulation for the Eddystone-URL
+ * Configuration Service Factory Reset characteristic.
+ */
+ WriteOnlyGattCharacteristic *factoryResetChar;
+
+ /**
+ * Pointer to the BLE API characteristic encapsulation for the Eddystone-GATT
+ * Configuration Service Remain Connectable characteristic.
+ */
+ ReadWriteGattCharacteristic *remainConnectableChar;
+
+ /**
+ * END OF GATT CHARACTERISTICS
+ */
+
+ /**
+ * EID: An array holding the slot next rotation times
+ */
+ SlotEidNextRotationTimes_t slotEidNextRotationTimes;
+
+ /**
+ * EID: Storage for the current slot encrypted EID Identity Key
+ */
+ EidIdentityKey_t encryptedEidIdentityKey;
+
+ /*
+ * Storage for all the slots / frames
+ */
+ SlotStorage_t slotStorage;
+
+ /**
+ * An array that defines the frame type of each slot using the slot number
+ * as an index.
+ */
+ SlotFrameTypes_t slotFrameTypes;
+
+ /**
+ * Circular buffer that represents of Eddystone frames to be advertised.
+ */
+ CircularBuffer advFrameQueue;
+
+ /**
+ * The registered callback to update the Eddystone-TLM frame Battery
+ * Voltage.
+ */
+ TlmUpdateCallback_t tlmBatteryVoltageCallback;
+
+ /**
+ * The registered callback to update the Eddystone-TLM frame Beacon
+ * Temperature.
+ */
+ TlmUpdateCallback_t tlmBeaconTemperatureCallback;
+
+ /**
+ * Type for the array of callback handles for all the slot timers
+ */
+ typedef event_queue_t::event_handle_t SlotCallbackHandles_t[MAX_ADV_SLOTS];
+
+ /**
+ * An array of all the slot timer callbacks handles
+ */
+ SlotCallbackHandles_t slotCallbackHandles;
+
+ /**
+ * Callback handle to keep track of manageRadio() callbacks.
+ */
+ event_queue_t::event_handle_t radioManagerCallbackHandle;
+
+ /**
+ * GattCharacteristic table used to populate the BLE ATT table in the
+ * GATT Server.
+ */
+ GattCharacteristic *charTable[TOTAL_CHARACTERISTICS];
+
+ /**
+ * Pointer to the device name currently being used.
+ */
+ const char *deviceName;
+
+ /**
+ * Defines an array of string constants (a container) used to initialise any URL slots
+ */
+ static const char* const slotDefaultUrls[];
+
+ /**
+ * Defines an array of UIDs to initialize UID slots
+ */
+ static const uint8_t slotDefaultUids[MAX_ADV_SLOTS][16];
+
+ /**
+ * Defines an array of EID (Identity keys) to initialize EID slots
+ */
+ static const uint8_t slotDefaultEidIdentityKeys[MAX_ADV_SLOTS][16];
+
+ /**
+ * Defines default EID payload before being updated with the first EID rotation value
+ */
+ static const uint8_t nullEid[8];
+
+ /**
+ * Reference to the event queue used to post tasks
+ */
+ event_queue_t& eventQueue;
+
+ /**
+ * Next EID slot frame that will be transmitted
+ */
+ uint8_t nextEidSlot;
+};
+
+#endif /* __EDDYSTONESERVICE_H__ */
diff --git a/implementations/mbed/source/EddystoneTypes.h b/implementations/mbed/source/EddystoneTypes.h
new file mode 100644
index 0000000..79c84f5
--- /dev/null
+++ b/implementations/mbed/source/EddystoneTypes.h
@@ -0,0 +1,239 @@
+/*
+ * Copyright (c) 2006-2016 Google Inc, All Rights Reserved
+ *
+ * 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.
+ */
+
+#ifndef __EDDYSTONETYPES_H__
+#define __EDDYSTONETYPES_H__
+
+#include
+#include
+#include "Eddystone_config.h"
+
+/**
+ * Macro to expand a 16-bit Eddystone UUID to 128-bit UUID.
+ */
+#define UUID_ES_BEACON(FIRST, SECOND) { \
+ 0xa3, 0x0c8, FIRST, SECOND, 0x8e, 0xd3, 0x4b, 0xdf, \
+ 0x8a, 0x39, 0xa0, 0x1b, 0xeb, 0xed, 0xe2, 0x95, \
+}
+
+/**
+ * Eddystone 16-bit UUID.
+ */
+const uint8_t EDDYSTONE_UUID[] = {0xAA, 0xFE};
+
+/**
+ * Size of Eddystone UID. Needed to construct all frames raw bytes.
+ */
+const uint16_t EDDYSTONE_UUID_SIZE = sizeof(EDDYSTONE_UUID);
+
+/** BEGINING OF CHARACTERISTICS */
+
+/**
+ * 128-bit UUID for Eddystone-GATT Configuration Service.
+ */
+const uint8_t UUID_ES_BEACON_SERVICE[] = UUID_ES_BEACON(0x75, 0x00);
+
+/**
+ * 128-bit UUID for Eddystone-URL Configuration Service Capabilities
+ * characteristic.
+ */
+const uint8_t UUID_CAPABILITIES_CHAR[] = UUID_ES_BEACON(0x75, 0x01);
+
+/**
+ * 128-bit UUID for Eddystone-URL Configuration Service Active Slot
+ * characteristic.
+ */
+const uint8_t UUID_ACTIVE_SLOT_CHAR[] = UUID_ES_BEACON(0x75, 0x02);
+
+/**
+ * 128-bit UUID for Eddystone-URL Configuration Service Advertising Interval
+ * characteristic.
+ */
+const uint8_t UUID_ADV_INTERVAL_CHAR[] = UUID_ES_BEACON(0x75, 0x03);
+
+/**
+ * 128-bit UUID for Eddystone-URL Configuration Service Radio Tx Power
+ * characteristic.
+ */
+const uint8_t UUID_RADIO_TX_POWER_CHAR[] = UUID_ES_BEACON(0x75, 0x04);
+
+/**
+ * 128-bit UUID for Eddystone-URL Configuration Service Adv Tx Power
+ * characteristic.
+ */
+const uint8_t UUID_ADV_TX_POWER_CHAR[] = UUID_ES_BEACON(0x75, 0x05);
+
+/**
+ * 128-bit UUID for Eddystone-URL Configuration Service Lock State
+ * characteristic.
+ */
+const uint8_t UUID_LOCK_STATE_CHAR[] = UUID_ES_BEACON(0x75, 0x06);
+
+/**
+ * 128-bit UUID for Eddystone-URL Configuration Service Unlock
+ * characteristic.
+ */
+const uint8_t UUID_UNLOCK_CHAR[] = UUID_ES_BEACON(0x75, 0x07);
+
+/**
+ * 128-bit UUID for Eddystone-URL Configuration Service Public ECDH Key
+ * characteristic.
+ */
+const uint8_t UUID_PUBLIC_ECDH_KEY_CHAR[] = UUID_ES_BEACON(0x75, 0x08);
+
+/**
+ * 128-bit UUID for Eddystone-URL Configuration Service EID Identity Key
+ * characteristic.
+ */
+const uint8_t UUID_EID_IDENTITY_KEY_CHAR[] = UUID_ES_BEACON(0x75, 0x09);
+
+/**
+ * 128-bit UUID for Eddystone-URL Configuration Service Adv Slot Data
+ * characteristic.
+ */
+const uint8_t UUID_ADV_SLOT_DATA_CHAR[] = UUID_ES_BEACON(0x75, 0x0a);
+
+/**
+ * 128-bit UUID for Eddystone-URL Configuration Service Reset
+ * characteristic.
+ */
+const uint8_t UUID_FACTORY_RESET_CHAR[] = UUID_ES_BEACON(0x75, 0x0b);
+
+/**
+ * 128-bit UUID for Eddystone-URL Configuration Service Remain Connectable
+ * characteristic.
+ */
+const uint8_t UUID_REMAIN_CONNECTABLE_CHAR[] = UUID_ES_BEACON(0x75, 0x0c);
+
+/** END OF CHARACTERISTICS */
+
+/**
+ * Default Lock State used by EddystoneService.
+ */
+const uint8_t DEFAULT_LOCK_STATE_DATA[] = {DEFAULT_LOCK_STATE};
+
+/**
+ * A type defining the size of the READ ONLY capability characteristic
+ */
+typedef uint8_t Capability_t[CAP_HDR_LEN + NUM_POWER_MODES];
+
+/**
+ * Type for the 128-bit for Eddystone-URL Configuration Service Lock and Unlock
+ * characteristic value.
+ */
+typedef uint8_t Lock_t[16];
+
+/**
+ * Type for the 128-bit for Eddystone-URL Configuration Service Advertised TX
+ * Power Levels characteristic value.
+ */
+typedef int8_t PowerLevels_t[NUM_POWER_MODES];
+
+/**
+ * Type representing the power level set for each slot
+ */
+typedef int8_t SlotTxPowerLevels_t[MAX_ADV_SLOTS];
+
+/**
+ * Type representing the adv interval set for each slot
+ */
+typedef uint16_t SlotAdvIntervals_t[MAX_ADV_SLOTS];
+
+/**
+ * Type representing the buffer used to represent the LockState and potentially
+ * an updated key
+ */
+typedef uint8_t LockState_t[17];
+
+/**
+ * Type representing the EID private ECDH Key
+ */
+typedef uint8_t PrivateEcdhKey_t[32];
+
+/**
+ * Type representing the EID public ECDH Key
+ */
+typedef uint8_t PublicEcdhKey_t[32];
+
+/**
+ * Type representing the EID Identity Key
+ */
+typedef uint8_t EidIdentityKey_t[16];
+
+/**
+ * Type representing the storage for a single slot
+ */
+typedef uint8_t Slot_t[32];
+
+/**
+ * Type representing the storage for all slots given MAX_ADV_SLOTS
+ */
+typedef uint8_t SlotStorage_t[MAX_ADV_SLOTS * sizeof(Slot_t)];
+
+/**
+ * Type representing the current frame types if each slot
+ */
+typedef uint8_t SlotFrameTypes_t[MAX_ADV_SLOTS];
+
+/**
+ * Type representing the EID rotation period exp for each slot
+ */
+typedef uint8_t SlotEidRotationPeriodExps_t[MAX_ADV_SLOTS];
+
+/**
+ * Type representing the EID next rotation time for each slot
+ */
+typedef uint32_t SlotEidNextRotationTimes_t[MAX_ADV_SLOTS];
+
+/**
+ * Type representing the EID identity keys for each slot
+ */
+typedef EidIdentityKey_t SlotEidIdentityKeys_t[MAX_ADV_SLOTS];
+
+/**
+ * Size in bytes of UID namespace ID.
+ */
+const size_t UID_NAMESPACEID_SIZE = 10;
+
+/**
+ * Type for the UID namespace ID.
+ */
+typedef uint8_t UIDNamespaceID_t[UID_NAMESPACEID_SIZE];
+
+/**
+ * Size in bytes of UID instance ID.
+ */
+const size_t UID_INSTANCEID_SIZE = 6;
+
+/**
+ * Type for the UID instance ID.
+ */
+typedef uint8_t UIDInstanceID_t[UID_INSTANCEID_SIZE];
+
+/**
+ * Type for callbacks to update Eddystone-TLM frame Batery Voltage and Beacon
+ * Temperature.
+ */
+typedef uint16_t (*TlmUpdateCallback_t) (uint16_t);
+
+// END OF PROTOTYPES
+
+typedef struct {
+ uint32_t timeInPriorBoots;
+ uint32_t timeSinceLastBoot;
+} TimeParams_t;
+
+#endif /* __EDDYSTONETYPES_H__ */
diff --git a/implementations/mbed/source/Eddystone_config.h b/implementations/mbed/source/Eddystone_config.h
new file mode 100644
index 0000000..087edcb
--- /dev/null
+++ b/implementations/mbed/source/Eddystone_config.h
@@ -0,0 +1,199 @@
+/*
+ * Copyright (c) 2016, Google, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+
+#ifndef EDDYSTONE_CONFIG_H_
+#define EDDYSTONE_CONFIG_H_
+
+// Version printed out on virtual terminal (independent of logging flag below)
+#define BUILD_VERSION_STR "EID Version 1.00 2016-11-19:15:00\r\n"
+
+/**
+ * Platform Target (if not set, default is nRF51-DK or nRF51-dongle or nRF52-DK)
+ * NOTE1: All targets are assumed to be 32K (in target.json) and S110 (in config.h)
+ * NOTE2: Only enable one target below (default is nRF_DK).
+ */
+// #define MinewTech51
+#define MinewTech52
+// #define nRF_DK
+
+/**
+ * DECLARE YOUR TARGET'S PARAMETERS
+ * If you are adding a new target, append to end of elif chain
+ *
+ * LED_OFF: value for an LED off state: 1 or 0
+ * CONFIG_LED: which LED to blink when in Configuration Mode
+ * On power up will go into configuration mode and eventually time out
+ * SHUTDOWN_LED: which LED to blink when shutting down (only used if RESET_BUTTON is defined)
+ * RESET_BUTTON: Which button to use. If defined, adds code to handle button presses
+ * Button press will toggle between configuration mode and off
+ * Configuration mode will eventually timeout and broadcast default values
+ * This will shutdown after initial power up! Assumes shipping with a battery in an off state
+ * EDDYSTONE_DEFAULT_RADIO_TX_POWER_LEVELS: Which power levels to offer
+ * EDDYSTONE_DEFAULT_ADV_TX_POWER_LEVELS: What to advertise these levels (as antennas always loose some power)
+ */
+#ifdef MinewTech51
+ #define LED_OFF 0
+ #define CONFIG_LED p15
+ #define SHUTDOWN_LED p16
+ #define RESET_BUTTON p18
+ #define EDDYSTONE_DEFAULT_RADIO_TX_POWER_LEVELS { -30, -16, -4, 4 }
+ #define EDDYSTONE_DEFAULT_ADV_TX_POWER_LEVELS { -42, -30, -25, -13 }
+
+#elif defined MinewTech52
+ #define LED_OFF 0
+ #define CONFIG_LED LED3
+ #define SHUTDOWN_LED LED2
+ #define RESET_BUTTON BUTTON1
+ #define EDDYSTONE_DEFAULT_RADIO_TX_POWER_LEVELS { -40, -20, -8, 4 }
+ #define EDDYSTONE_DEFAULT_ADV_TX_POWER_LEVELS { -50, -30, -18, -6 }
+
+#else
+ // *** nRF_DK or USB Dongle PIN defines ***
+ #define LED_OFF 1
+ #define CONFIG_LED LED3
+ // Uncomment the defines below if you want the DK board to behave like a
+ // Beacon target with shutdown on power up, and a mode button
+ // #define SHUTDOWN_LED LED2
+ // #define RESET_BUTTON BUTTON1
+ #define EDDYSTONE_DEFAULT_RADIO_TX_POWER_LEVELS { -30, -16, -4, 4 }
+ #define EDDYSTONE_DEFAULT_ADV_TX_POWER_LEVELS { -42, -30, -25, -13 }
+#endif
+
+/**
+ * DEBUG OPTIONS
+ * For production: all defines below should be UNCOMMENTED:
+ * Key
+ * GEN_BEACON_KEYS_AT_INIT: Debugging flag to help test entropy source
+ * HARDWARE_RANDOM_NUM_GENERATOR: include if the target supports a hardware RNG
+ * EID_RANDOM_MAC: include if you want to randomize the mac address for each eid rotation
+ * INCLUDE_CONFIG_URL: Includes configuration url when in Configuration Mode
+ * DONT_REMAIN_CONNECTABLE: Debugging flag; remain connectable during beaconing for easy testing
+ * NO_4SEC_START_DELAY: Debugging flag to pause 4s before starting; allow time to connect virtual terminal
+ * NO_EAX_TEST: Debugging flag: when not define, test will check x = EAX_DECRYPT(EAX_ENCRYPT(x)), output in LOG
+ * NO_LOGGING: Debugging flag; controls logging to virtual terminal
+ */
+#define GEN_BEACON_KEYS_AT_INIT
+#define HARDWARE_RANDOM_NUM_GENERATOR
+#define EID_RANDOM_MAC
+#define INCLUDE_CONFIG_URL
+#define DONT_REMAIN_CONNECTABLE
+#define NO_4SEC_START_DELAY
+#define NO_EAX_TEST
+#define NO_LOGGING
+
+/* Default enable printf logging, unless explicitly NO_LOGGING */
+#ifdef NO_LOGGING
+ #define LOG_PRINT 0
+#else
+ #define LOG_PRINT 1
+#endif
+
+#define LOG(x) do { if (LOG_PRINT) printf x; } while (0)
+
+/**
+ * GENERIC BEACON BEHAVIORS DEFINED
+ * Note: If the CONFIG_URL is enabled (DEFINE above)
+ * The size of the DEVICE_NAME + Encoded Length of the CONFIG_URL
+ * must be LESS THAN OR EQUAL to 23
+ */
+#define EDDYSTONE_CONFIG_URL "http://c.pw3b.com"
+#define EDDYSTONE_CFG_DEFAULT_DEVICE_NAME "Eddystone v3.0"
+#define EDDYSTONE_DEFAULT_MAX_ADV_SLOTS 3
+#define EDDYSTONE_DEFAULT_CONFIG_ADV_INTERVAL 1000
+#define EDDYSTONE_DEFAULT_CONFIG_ADVERTISEMENT_TIMEOUT_SECONDS 60
+
+#define EDDYSTONE_DEFAULT_UNLOCK_KEY { \
+ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF \
+}
+
+#define EDDYSTONE_DEFAULT_SLOT_URLS { \
+ "http://c.pw3b.com", \
+ "https://www.mbed.com/", \
+ "https://www.github.com/" \
+}
+
+#define EDDYSTONE_DEFAULT_SLOT_UIDS { \
+ { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F }, \
+ { 0x00, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, 0xF0 }, \
+ { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF } \
+}
+
+#define EDDYSTONE_DEFAULT_SLOT_EID_IDENTITY_KEYS { \
+ { 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5, 0xA6, 0xA7, 0xA8, 0xA9, 0xAA, 0xAB, 0xAC, 0xAD, 0xAE, 0xAF }, \
+ { 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xBA, 0xBB, 0xBC, 0xBD, 0xBE, 0xBF }, \
+ { 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF } \
+}
+
+#define EDDYSTONE_DEFAULT_SLOT_EID_ROTATION_PERIOD_EXPS { 10, 10, 10 }
+
+// The following frame/slot types are supported: URL, UID, TLM, EID. The defaults set URL x2 and EID
+#define EDDYSTONE_DEFAULT_SLOT_TYPES { \
+ EDDYSTONE_FRAME_URL, \
+ EDDYSTONE_FRAME_URL, \
+ EDDYSTONE_FRAME_EID \
+}
+
+#define EDDYSTONE_DEFAULT_SLOT_INTERVALS { 700, 0, 0 }
+
+#define EDDYSTONE_DEFAULT_SLOT_TX_POWERS { -8, -8, -8 }
+
+/**
+ * Lock constants
+ */
+#define LOCKED 0
+#define UNLOCKED 1
+#define UNLOCKED_AUTO_RELOCK_DISABLED 2
+
+#define DEFAULT_LOCK_STATE UNLOCKED
+
+/**
+ * Set default number of adv slots
+ */
+const uint8_t MAX_ADV_SLOTS = EDDYSTONE_DEFAULT_MAX_ADV_SLOTS;
+
+/**
+ * Slot and Power and Interval Constants
+ */
+const uint8_t DEFAULT_SLOT = 0;
+
+/**
+ * Number of radio power modes supported
+ */
+const uint8_t NUM_POWER_MODES = 4;
+
+/**
+ * Default name for the BLE Device Name characteristic.
+ */
+const char DEFAULT_DEVICE_NAME[] = EDDYSTONE_CFG_DEFAULT_DEVICE_NAME;
+
+/**
+ * ES GATT Capability Constants (6 values)
+ */
+const uint8_t CAP_HDR_LEN = 6; // The six constants below
+const uint8_t ES_GATT_VERSION = 0;
+const uint8_t MAX_EIDS = MAX_ADV_SLOTS;
+const uint8_t CAPABILITIES = 0x03; // Per slot variable interval and variable Power
+const uint8_t SUPPORTED_FRAMES_H = 0x00;
+const uint8_t SUPPORTED_FRAMES_L = 0x0F;
+
+/**
+ * ES GATT Capability Constant Array storing the capability constants
+ */
+const uint8_t CAPABILITIES_DEFAULT[] = {ES_GATT_VERSION, MAX_ADV_SLOTS, MAX_EIDS, CAPABILITIES, \
+ SUPPORTED_FRAMES_H, SUPPORTED_FRAMES_L};
+
+#endif /* EDDYSTONE_CONFIG_H_ */
diff --git a/implementations/mbed/source/EntropySource/EntropySource.cpp b/implementations/mbed/source/EntropySource/EntropySource.cpp
new file mode 100644
index 0000000..793cede
--- /dev/null
+++ b/implementations/mbed/source/EntropySource/EntropySource.cpp
@@ -0,0 +1,33 @@
+/* mbed Microcontroller Library
+ * Copyright (c) 2006-2015 ARM Limited
+ *
+ * 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.
+ */
+#include "EntropySource.h"
+
+#if !defined(TARGET_NRF51822) && !defined(TARGET_MCU_NRF52832) /* Persistent storage supported on nrf51 platforms */
+ /**
+ * When not using an nRF51-based target then entropy source is currently unimplemented.
+ */
+ #error "INSECURE CONFIGURATION - YOU MUST IMPLEMENT AN ENTROPY SOURCE"
+
+ int eddystoneRegisterEntropySource( mbedtls_entropy_context* ctx) {
+ return 1;
+ }
+
+ int eddystoneEntropyPoll( void *data,
+ unsigned char *output, size_t len, size_t *olen )
+ {
+ return( 1 );
+ }
+#endif /* #ifdef TARGET_NRF51822 */
diff --git a/implementations/mbed/source/EntropySource/EntropySource.h b/implementations/mbed/source/EntropySource/EntropySource.h
new file mode 100644
index 0000000..a1c73c0
--- /dev/null
+++ b/implementations/mbed/source/EntropySource/EntropySource.h
@@ -0,0 +1,11 @@
+#ifndef __ENTROPY_SOURCE_H__
+#define __ENTROPY_SOURCE_H__
+#include
+#include
+
+int eddystoneRegisterEntropySource( mbedtls_entropy_context* ctx);
+
+int eddystoneEntropyPoll( void *data,
+ unsigned char *output, size_t len, size_t *olen );
+
+#endif // __ENTROPY_SOURCE_H__
diff --git a/implementations/mbed/source/EntropySource/nRFEntropySource/nRFEntropySource.cpp b/implementations/mbed/source/EntropySource/nRFEntropySource/nRFEntropySource.cpp
new file mode 100644
index 0000000..3e32958
--- /dev/null
+++ b/implementations/mbed/source/EntropySource/nRFEntropySource/nRFEntropySource.cpp
@@ -0,0 +1,67 @@
+/* mbed Microcontroller Library
+ * Copyright (c) 2006-2015 ARM Limited
+ *
+ * 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.
+ */
+#include "../EntropySource.h"
+
+#if defined(TARGET_NRF51822) || defined(TARGET_MCU_NRF52832) /* Persistent storage supported on nrf51 platforms */
+
+#include "nrf_soc.h"
+#include "nrf_error.h"
+#include "mbed.h"
+#include
+
+/*
+ * nRF51 has a TRNG that we can access using SoftDevice.
+ */
+int eddystoneEntropyPoll(void *data, unsigned char *output, size_t len, size_t *olen)
+{
+ uint8_t bytes_available = 0;
+
+ // get the number of random bytes available
+ if (sd_rand_application_bytes_available_get(&bytes_available) != NRF_SUCCESS) {
+ return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED;
+ }
+
+ // if there is more bytes available that what is requested,
+ // truncate the number of bytes in output to len, otherwise use the total
+ // of bytes available.
+ const uint8_t output_len = bytes_available > len ? len : bytes_available;
+
+ if (output_len) {
+ // transfer "output_len" random bytes to output.
+ if (sd_rand_application_vector_get(output, output_len) != NRF_SUCCESS) {
+ return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED;
+ }
+ }
+
+ // Everything went fine, commit the output_len to the output parameter
+ *olen = output_len;
+ return 0;
+}
+
+int eddystoneRegisterEntropySource( mbedtls_entropy_context* ctx) {
+ uint8_t pool_capacity;
+ sd_rand_application_pool_capacity_get(&pool_capacity);
+
+ return mbedtls_entropy_add_source(
+ ctx,
+ eddystoneEntropyPoll, // entropy source function
+ NULL, // entropy source data, NULL in this case
+ pool_capacity, // minimum number of bytes the entropy pool should wait on from this callback before releasing entropy
+ MBEDTLS_ENTROPY_SOURCE_STRONG
+ );
+}
+
+#endif /* #ifdef TARGET_NRF51822 */
diff --git a/implementations/mbed/source/EventQueue/AlignedStorage.h b/implementations/mbed/source/EventQueue/AlignedStorage.h
new file mode 100644
index 0000000..0940360
--- /dev/null
+++ b/implementations/mbed/source/EventQueue/AlignedStorage.h
@@ -0,0 +1,207 @@
+/*
+ * Copyright (c) 2016, ARM Limited, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+#ifndef EVENTQUEUE_DETAIL_ALIGNEDSTORAGE_H_
+#define EVENTQUEUE_DETAIL_ALIGNEDSTORAGE_H_
+
+#include
+
+namespace eq {
+
+class AnonymousDeclaration;
+
+/**
+ * Provide aligned raw storage for holding a type T.
+ * This class is useful to delay the construction of objects while reserving
+ * space in memory for them.
+ * For instance, it can be use with static or global variable which can not
+ * be constructed before main. It can also be used in class member which
+ * do not have or can't be constructed at parent construction time.
+ *
+ * Once the storage has been reserved, it is possible explicitly construct the
+ * object of T by using placement new syntax.
+ * @code
+ * AlignedStorage foo;
+ * new (foo.get_storage()) Foo(...);
+ * @endcode
+ * Then it is possible to get a reference to the object by calling the member
+ * function get.
+ * @code
+ * foo.get().doSomething();
+ * @endcode
+ * Once the object needs to be destroyed it is possible to call the destructor
+ * directly
+ * @code
+ * foo.get().~Foo();
+ * @endcode
+ * After this point, their is no instance of T in the storage and the function
+ * get remains unusable until an object is again initialised in the storage.
+ */
+template
+class AlignedStorage {
+public:
+ /**
+ * Initialisation of the storage, does **not** zeroed its memory.
+ */
+ AlignedStorage() {}
+ /**
+ * Provide the raw pointer to the address of the storage.
+ */
+ void* get_storage() {
+ return data;
+ }
+
+ /**
+ * Provide the raw pointer to the const address of the storage.
+ */
+ const void* get_storage() const {
+ return data;
+ }
+
+ /**
+ * Return a reference to the element T in this storage.
+ */
+ T& get() {
+ return *static_cast(get_storage());
+ }
+
+ /**
+ * Return a reference to the const element of T in this storage.
+ */
+ const T& get() const {
+ return *static_cast(get_storage());
+ }
+
+private:
+ // it doesn't make sense to allow copy construction of copy assignement for
+ // this kind of object.
+ AlignedStorage(const AlignedStorage&);
+ AlignedStorage& operator=(const AlignedStorage&);
+ // storage. Can be improved by metaprogramming to be the best fit.
+ union {
+ char char_storage;
+ short int short_int_storage;
+ int int_storage;
+ long int long_int_storage;
+ float float_storage;
+ double double_storage;
+ long double long_double_storage;
+ void* pointer_storage;
+ AnonymousDeclaration (*function_pointer_storage)(AnonymousDeclaration);
+ AnonymousDeclaration* AnonymousDeclaration::*data_member_storage ;
+ AnonymousDeclaration (AnonymousDeclaration::*function_member_storage)(AnonymousDeclaration);
+ char data[sizeof(T)];
+ };
+};
+
+/**
+ * Provide aligned raw storage for holding an array of type T.
+ * This is a specialisation of AlignedStorage for arrays of T.
+ * With this class, it is possible to reserve space for a given number of
+ * elements of type T and delay their construction to a latter point. This
+ * feature can be really useful when building generic container which
+ * embed memory for their elements. Instead of default constructing them,
+ * the construction of an element can be made when it is really needed, by
+ * copy. It is the same for the destruction, only objects which have been
+ * constructed needs to be destructed.
+ * Those properties improve generic containers because only the operations
+ * which have to be made are made. It also allow generic container to hold
+ * types which are not DefaultConstructible.
+ *
+ * Once the storage has been reserved, it is possible explicitly construct an
+ * object of T at a given index by using placement new syntax.
+ * @code
+ * AlignedStorage foo;
+ * //construct object at index 0 then at index 1.
+ * new (foo.get_storage(0)) Foo(...);
+ * new (foo.get_storage(1)) Foo(...);
+ * @endcode
+ * Then it is possible to get a reference to an object at a given index by
+ * calling the member function get.
+ * @code
+ * // do something with object at index 1
+ * foo.get(1).doSomething();
+ * @endcode
+ * Once the object needs to be destroyed it is possible to call the destructor
+ * directly
+ * @code
+ * // destroy object at index 1.
+ * foo.get(1).~Foo();
+ * @endcode
+ * After this point, their is no instance of T at index 1 in the storage and
+ * trying to use the object at this index will lead to undefined behavior until
+ * an object is again initialised at this index.
+ */
+template
+struct AlignedStorage {
+ /**
+ * Initialisation of the storage, does **not** zeroed its memory.
+ */
+ AlignedStorage() {}
+
+ /**
+ * Return raw pointer to the address of element at a given index
+ */
+ void* get_storage(std::size_t index) {
+ return &get(index);
+ }
+
+ /**
+ * const version of void* get_storage(std::size_t).
+ */
+ const void* get_storage(std::size_t index) const {
+ return &get(index);
+ }
+
+ /**
+ * Return reference to the element stored atindex.
+ */
+ T& get(std::size_t index) {
+ return reinterpret_cast(data)[index];
+ }
+
+ /**
+ * const version of T& get(std::size_t).
+ */
+ const T& get(std::size_t index) const {
+ return reinterpret_cast(data)[index];
+ }
+
+private:
+ // it doesn't make sense to allow copy construction of copy assignement for
+ // this kind of object.
+ AlignedStorage(const AlignedStorage&);
+ AlignedStorage& operator=(const AlignedStorage&);
+ // storage. Can be improved by metaprogramming to be the best fit.
+ union {
+ char char_storage;
+ short int short_int_storage;
+ int int_storage;
+ long int long_int_storage;
+ float float_storage;
+ double double_storage;
+ long double long_double_storage;
+ void* pointer_storage;
+ AnonymousDeclaration (*function_pointer_storage)(AnonymousDeclaration);
+ AnonymousDeclaration* AnonymousDeclaration::*data_member_storage ;
+ AnonymousDeclaration (AnonymousDeclaration::*function_member_storage)(AnonymousDeclaration);
+ char data[sizeof(T[ArraySize])];
+ };
+};
+
+} // namespace eq
+
+#endif /* EVENTQUEUE_DETAIL_ALIGNEDSTORAGE_H_ */
diff --git a/implementations/mbed/source/EventQueue/EventQueue.h b/implementations/mbed/source/EventQueue/EventQueue.h
new file mode 100644
index 0000000..46e927e
--- /dev/null
+++ b/implementations/mbed/source/EventQueue/EventQueue.h
@@ -0,0 +1,127 @@
+/*
+ * Copyright (c) 2016, ARM Limited, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+#ifndef EVENTQUEUE_EVENTQUEUE_H_
+#define EVENTQUEUE_EVENTQUEUE_H_
+
+#include
+#include "Thunk.h"
+#include "MakeThunk.h"
+
+namespace eq {
+
+class EventQueue {
+
+public:
+ /// typedef for callable type.
+ /// the callable type used should support the same operations
+ /// supported by a void(*)() function pointer.
+ typedef Thunk function_t;
+
+ /// handle to a posted event which will be executed later.
+ /// model after a void* pointer.
+ typedef void* event_handle_t;
+
+ /// type used for time
+ typedef std::size_t ms_time_t;
+
+ /// Construct an empty event queue
+ EventQueue() { }
+
+ virtual ~EventQueue() { }
+
+ /**
+ * Post a callable to the event queue.
+ * It will be executed during the next dispatch cycle.
+ * @param f The callbable to be executed by the event queue.
+ * @return the handle to the event.
+ */
+ template
+ event_handle_t post(const F& fn) {
+ return do_post(fn);
+ }
+
+ /**
+ * Bind a callable and an argument then post a callable to the event queue.
+ * It will be executed during the next dispatch cycle.
+ * @param f The callbable to be bound with arg0.
+ * @param arg0 The first argument to bind to f.
+ * @return the handle to the event.
+ */
+ template
+ event_handle_t post(const F& fn, const Arg0& arg0) {
+ return do_post(make_thunk(fn, arg0));
+ }
+
+ template
+ event_handle_t post(const F& fn, const Arg0& arg0, const Arg1& arg1) {
+ return do_post(make_thunk(fn, arg0, arg1));
+ }
+
+ template
+ event_handle_t post(const F& fn, const Arg0& arg0, const Arg1& arg1, const Arg2& arg2) {
+ return do_post(make_thunk(fn, arg0, arg1, arg2));
+ }
+
+ template
+ event_handle_t post_in(const F& fn, ms_time_t ms_delay) {
+ return do_post(fn, ms_delay);
+ }
+
+ template
+ event_handle_t post_in(const F& fn, const Arg0& arg0, ms_time_t ms_delay) {
+ return do_post(make_thunk(fn, arg0), ms_delay);
+ }
+
+ template
+ event_handle_t post_in(const F& fn, const Arg0& arg0, const Arg1& arg1, ms_time_t ms_delay) {
+ return do_post(make_thunk(fn, arg0, arg1), ms_delay);
+ }
+
+ template
+ event_handle_t post_in(const F& fn, const Arg0& arg0, const Arg1& arg1, const Arg2& arg2, ms_time_t ms_delay) {
+ return do_post(make_thunk(fn, arg0, arg1, arg2), ms_delay);
+ }
+
+ template
+ event_handle_t post_every(const F& fn, ms_time_t ms_delay) {
+ return do_post(fn, ms_delay, true);
+ }
+
+ template
+ event_handle_t post_every(const F& fn, const Arg0& arg0, ms_time_t ms_delay) {
+ return do_post(make_thunk(fn, arg0), ms_delay, true);
+ }
+
+ template
+ event_handle_t post_every(const F& fn, const Arg0& arg0, const Arg1& arg1, ms_time_t ms_delay) {
+ return do_post(make_thunk(fn, arg0, arg1), ms_delay, true);
+ }
+
+ template
+ event_handle_t post_every(const F& fn, const Arg0& arg0, const Arg1& arg1, const Arg2& arg2, ms_time_t ms_delay) {
+ return do_post(make_thunk(fn, arg0, arg1, arg2), ms_delay, true);
+ }
+
+ virtual bool cancel(event_handle_t event_handle) = 0;
+
+private:
+ virtual event_handle_t do_post(const function_t& fn, ms_time_t ms_delay = 0, bool repeat = false) = 0;
+};
+
+} // namespace eq
+
+#endif /* EVENTQUEUE_EVENTQUEUE_H_ */
diff --git a/implementations/mbed/source/EventQueue/EventQueueClassic.h b/implementations/mbed/source/EventQueue/EventQueueClassic.h
new file mode 100644
index 0000000..d3ec007
--- /dev/null
+++ b/implementations/mbed/source/EventQueue/EventQueueClassic.h
@@ -0,0 +1,251 @@
+/*
+ * Copyright (c) 2016, ARM Limited, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+#ifndef BLE_API_SOURCE_MBEDCLASSICEVENTQUEUE_H_
+#define BLE_API_SOURCE_MBEDCLASSICEVENTQUEUE_H_
+
+#include
+#include "PriorityQueue.h"
+#include "Ticker.h"
+#include "Timer.h"
+#include
+#include "Thunk.h"
+#include "MakeThunk.h"
+#include "EventQueue.h"
+
+#include
+typedef ::mbed::util::CriticalSectionLock CriticalSection;
+
+namespace eq {
+
+template
+class EventQueueClassic: public EventQueue {
+
+ /// Describe an event.
+ /// An event is composed of a function f to execute after a a time t.
+ /// Optionnaly, the event can be periodic and in this case the function f
+ /// is executed after each period p.
+ struct Event {
+ /// construct an event
+ /// @param f The function to execute when this event occur
+ /// @param ms_remaining_time remaining time before this event occurence
+ /// @param ms_repeat_period If the event is periodic, this parameter is the
+ /// period between to occurence of this event.
+ Event(const function_t& f, ms_time_t ms_remaining_time, ms_time_t ms_repeat_period = 0) :
+ _f(f),
+ _ms_remaining_time(ms_remaining_time),
+ _ms_repeat_period(ms_repeat_period) {
+ }
+
+ /// call the inner function within an event
+ void operator()() {
+ _f();
+ }
+
+ /// return a reference to the inner function
+ const function_t& get_function() const {
+ return _f;
+ }
+
+ /// comparison operator used by the priority queue.
+ /// comaprare remaining time between two events
+ friend bool operator<(const Event& lhs, const Event& rhs) {
+ return lhs._ms_remaining_time < rhs._ms_remaining_time;
+ }
+
+ /// return the time remaining when this event was inserted into the priority queue.
+ ms_time_t get_ms_remaining_time() const {
+ return _ms_remaining_time;
+ }
+
+ /// update the remaining time for this event
+ void set_ms_remaining_time(ms_time_t new_remaining_time) {
+ _ms_remaining_time = new_remaining_time;
+ }
+
+ /// If an event is periodic, return the time between two occurence
+ ms_time_t get_ms_repeat_period() const {
+ return _ms_repeat_period;
+ }
+
+ private:
+ function_t _f;
+ ms_time_t _ms_remaining_time;
+ const ms_time_t _ms_repeat_period;
+ };
+
+ /// type of the internal queue
+ typedef PriorityQueue priority_queue_t;
+
+ /// iterator for the queue type
+ typedef typename priority_queue_t::iterator q_iterator_t;
+
+ /// node type in the queue
+ typedef typename priority_queue_t::Node q_node_t;
+
+public:
+ /// Construct an empty event queue
+ EventQueueClassic() :
+ _events_queue(), _ticker(), _timer(), _timed_event_pending(false) {
+ }
+
+ virtual ~EventQueueClassic() { }
+
+ virtual bool cancel(event_handle_t event_handle) {
+ CriticalSection critical_section;
+ bool success = _events_queue.erase(static_cast(event_handle));
+ if (success) {
+ // update the timers and events remaining time
+ updateTime();
+ }
+ return success;
+ }
+
+ void dispatch() {
+ while(true) {
+ function_t f;
+ // pick a task from the queue/ or leave
+ {
+ CriticalSection cs;
+ q_iterator_t event_it = _events_queue.begin();
+ if(event_it != _events_queue.end() && event_it->get_ms_remaining_time() == 0) {
+ f = event_it->get_function();
+ // if the event_it should be repeated, reschedule it
+ if (event_it->get_ms_repeat_period()) {
+ reschedule_event(event_it);
+ } else {
+ _events_queue.pop();
+ }
+ } else {
+ break;
+ }
+ }
+ f();
+ }
+ }
+
+private:
+
+ void update_ticker(ms_time_t ms_delay) {
+ _timed_event_pending = true;
+ _ticker.detach();
+ _ticker.attach(this, &EventQueueClassic::updateTime, ((float) ms_delay / 1000));
+ }
+
+ void update_ticker(q_node_t* ref, ms_time_t ms_delay) {
+ // look if the node inserted is the first node with a delay
+ for (q_iterator_t it = _events_queue.begin(); it != _events_queue.end(); ++it) {
+ if(it->get_ms_remaining_time()) {
+ if (it.get_node() == ref) {
+ // update the ticker to ms_delay if the first event
+ // with a delay is the one inserted
+ update_ticker(ms_delay);
+ }
+ break;
+ }
+ }
+ }
+
+ void update_events_remaining_time(ms_time_t elapsed_time) {
+ bool ticker_updated = false;
+
+ for (q_iterator_t it = _events_queue.begin();
+ it != _events_queue.end(); ++it) {
+ ms_time_t remaining_time = it->get_ms_remaining_time();
+ if(remaining_time) {
+ if(remaining_time <= elapsed_time) {
+ it->set_ms_remaining_time(0);
+ } else {
+ it->set_ms_remaining_time(remaining_time - elapsed_time);
+ if (!ticker_updated) {
+ update_ticker(it->get_ms_remaining_time());
+ _timer.start();
+ ticker_updated = true;
+ }
+ }
+ }
+ }
+ }
+
+ void updateTime() {
+ CriticalSection critical_section;
+ ms_time_t elapsed_time = _timer.read_ms();
+ _timed_event_pending = false;
+ _timer.stop();
+ _timer.reset();
+ _ticker.detach();
+ update_events_remaining_time(elapsed_time);
+ }
+
+ void reschedule_event(q_iterator_t& event_it) {
+ ms_time_t ms_period = event_it->get_ms_repeat_period();
+
+ if (_timed_event_pending == false) {
+ update_ticker(ms_period);
+ _timer.start();
+ event_it->set_ms_remaining_time(ms_period);
+ _events_queue.update(event_it);
+ } else {
+ int elapsed_time = _timer.read_ms();
+ event_it->set_ms_remaining_time(elapsed_time + ms_period);
+ _events_queue.update(event_it);
+ update_ticker(event_it.get_node(), ms_period);
+ }
+ }
+
+ virtual event_handle_t do_post(const function_t& fn, ms_time_t ms_delay = 0, bool repeat = false) {
+ if(repeat && (ms_delay == 0)) {
+ return NULL;
+ }
+
+ Event event(fn, ms_delay, repeat ? ms_delay : 0);
+
+ CriticalSection critical_section;
+ if (_events_queue.full()) {
+ return NULL;
+ }
+
+ // there is no need to update timings if ms_delay == 0
+ if (!ms_delay) {
+ return _events_queue.push(event).get_node();
+ }
+
+ // if there is no pending timed event, just add this one and start timers
+ if (_timed_event_pending == false) {
+ update_ticker(ms_delay);
+ _timer.start();
+ return _events_queue.push(event).get_node();
+ }
+
+ int elapsed_time = _timer.read_ms();
+
+ // update remaining time and post the event
+ event.set_ms_remaining_time(ms_delay + elapsed_time);
+ event_handle_t handle = _events_queue.push(event).get_node();
+ update_ticker(static_cast(handle), ms_delay);
+
+ return handle;
+ }
+
+ priority_queue_t _events_queue;
+ mbed::Ticker _ticker;
+ mbed::Timer _timer;
+ bool _timed_event_pending;
+};
+
+} // namespace eq
+
+#endif /* BLE_API_SOURCE_MBEDCLASSICEVENTQUEUE_H_ */
diff --git a/implementations/mbed/source/EventQueue/EventQueueMinar.h b/implementations/mbed/source/EventQueue/EventQueueMinar.h
new file mode 100644
index 0000000..a00d774
--- /dev/null
+++ b/implementations/mbed/source/EventQueue/EventQueueMinar.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2016, ARM Limited, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+#ifndef EVENTQUEUE_EVENTQUEUEMINAR_H_
+#define EVENTQUEUE_EVENTQUEUEMINAR_H_
+
+#include
+#include "EventQueue.h"
+
+namespace eq {
+
+class EventQueueMinar: public EventQueue {
+
+public:
+ /// Construct an empty event queue
+ EventQueueMinar() { }
+
+ virtual ~EventQueueMinar() { }
+
+ virtual bool cancel(event_handle_t event_handle) {
+ return minar::Scheduler::cancelCallback(event_handle);
+ }
+
+private:
+
+ virtual event_handle_t do_post(const function_t& fn, ms_time_t ms_delay = 0, bool repeat = false) {
+ // convert ms to minar time
+ minar::tick_t tick = minar::milliseconds(ms_delay);
+
+ // convert thunk to minar FunctionPointerBind
+ mbed::util::Event func(
+ mbed::util::FunctionPointer1(
+ free_func_thunk_call
+ ).bind(fn)
+ );
+
+ if (ms_delay == 0) {
+ return minar::Scheduler::postCallback(func).getHandle();
+ }
+
+ if (repeat == false) {
+ return minar::Scheduler::postCallback(func).delay(tick).tolerance(0).getHandle();
+ } else {
+ return minar::Scheduler::postCallback(func).period(tick).tolerance(0).getHandle();
+ }
+ }
+
+ // due to design limitations in function pointer classes, it is not possible
+ // to use reference here ...
+ static void free_func_thunk_call(function_t fn) {
+ fn();
+ }
+};
+
+} // namespace eq
+
+#endif /* EVENTQUEUE_EVENTQUEUEMINAR_H_ */
diff --git a/implementations/mbed/source/EventQueue/MakeThunk.h b/implementations/mbed/source/EventQueue/MakeThunk.h
new file mode 100644
index 0000000..739ee2f
--- /dev/null
+++ b/implementations/mbed/source/EventQueue/MakeThunk.h
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2016, ARM Limited, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+#ifndef EVENTQUEUE_MAKETHUNK_H
+#define EVENTQUEUE_MAKETHUNK_H
+
+#include "detail/MemberFunctionAdaptor.h"
+#include "detail/FunctionAdaptor.h"
+#include "detail/Thunks.h"
+
+namespace eq {
+
+/**
+ * Make a thunk from an F.
+ * When this function only takes an F then F it is expected that F is already
+ * a callable and therefore a kind of thunk.
+ * @tparam F The type of callable in input.
+ * @param fn the function to turn into a thunk.
+ * @return fn
+ */
+template
+const F& make_thunk(const F& fn) {
+ return fn;
+}
+
+/**
+ * Bind fn and arg0 into a thunk.
+ * @tparam F the type of the function to bind. It can be a function pointer,
+ * a function like object or a pointer to a member function.
+ * @tparam Arg0 The type of the first argument of F.
+ * @param fn the function to bind.
+ * @param arg0 the first argument to bind.
+ * @return a thunk binding F and arg0.
+ */
+template
+detail::Thunk_1::type, Arg0>
+make_thunk(const F& fn, const Arg0& arg0) {
+ typedef typename detail::FunctionAdaptor::type fn_adaptor_t;
+ return detail::Thunk_1(
+ fn_adaptor_t(fn),
+ arg0
+ );
+}
+
+/**
+ * Bind fn, arg0 and arg1 into a thunk.
+ * @tparam F the type of the function to bind. It can be a function pointer,
+ * a function like object or a pointer to a member function.
+ * @tparam Arg0 The type of the first argument of F.
+ * @tparam Arg1 The type of the second argument of F.
+ * @param fn the function to bind.
+ * @param arg0 the first argument to bind.
+ * @param arg1 the second argument to bind.
+ * @return a thunk binding F, arg0 and arg1.
+ */
+template
+detail::Thunk_2::type, Arg0, Arg1>
+make_thunk(const F& fn, const Arg0& arg0, const Arg1& arg1) {
+ typedef typename detail::FunctionAdaptor::type fn_adaptor_t;
+ return detail::Thunk_2(
+ fn_adaptor_t(fn),
+ arg0,
+ arg1
+ );
+}
+
+/**
+ * Bind fn, arg0, arg1 and arg2 into a thunk.
+ * @tparam F the type of the function to bind. It can be a function pointer,
+ * a function like object or a pointer to a member function.
+ * @tparam Arg0 The type of the first argument of F.
+ * @tparam Arg1 The type of the second argument of F.
+ * @tparam Arg2 The type of the third argument of F.
+ * @param fn the function to bind.
+ * @param arg0 the first argument to bind.
+ * @param arg1 the second argument to bind.
+ * @param arg1 the third argument to bind.
+ * @return a thunk binding F, arg0, arg1 and arg2.
+ */
+template
+detail::Thunk_3::type, Arg0, Arg1, Arg2>
+make_thunk(const F& fn, const Arg0& arg0, const Arg1& arg1, const Arg2& arg2) {
+ typedef typename detail::FunctionAdaptor::type fn_adaptor_t;
+ return detail::Thunk_3(
+ fn_adaptor_t(fn),
+ arg0,
+ arg1,
+ arg2
+ );
+}
+
+} // namespace eq
+
+#endif /* EVENTQUEUE_MAKETHUNK_H */
diff --git a/implementations/mbed/source/EventQueue/PriorityQueue.h b/implementations/mbed/source/EventQueue/PriorityQueue.h
new file mode 100644
index 0000000..4738e4b
--- /dev/null
+++ b/implementations/mbed/source/EventQueue/PriorityQueue.h
@@ -0,0 +1,418 @@
+/*
+ * Copyright (c) 2016, ARM Limited, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+#ifndef EVENTQUEUE_STACKPRIORITYQUEUE_H_
+#define EVENTQUEUE_STACKPRIORITYQUEUE_H_
+
+#include
+#include "AlignedStorage.h"
+
+namespace eq {
+
+/**
+ * Priority queue of Ts.
+ * Ts are ordered from the smaller to the bigger ( < ).
+ * Elements in the queue are mutable (this is a design choice).
+ * After a mutation the function update should be called to ensure that the
+ * queue is still properly sorted.
+ * @tparam T type of elements in this queue
+ * @param capacity Number of elements that this queue can contain
+ */
+template
+class PriorityQueue {
+
+public:
+ /**
+ * Type of the nodes in this queue.
+ */
+ struct Node {
+ AlignedStorage storage; /// storage for the T
+ Node* next; /// pointer to the next node
+ };
+
+ /**
+ * Iterator for elements of the queue.
+ */
+ class Iterator {
+ friend PriorityQueue;
+
+ /// Construct an iterator from a Node.
+ /// This constructor is private and can only be invoked from the PriorityQueue.
+ Iterator(Node* current) :
+ _current(current) {
+ }
+
+ public:
+
+ /// Indirection operator.
+ /// return a reference to the inner T
+ T& operator*() {
+ return _current->storage.get();
+ }
+
+ /// Const version of indirection operator.
+ /// return a reference to the inner T
+ const T& operator*() const {
+ return _current->storage.get();
+ }
+
+ /// dereference operator.
+ /// Will invoke the operation on the inner T
+ T* operator->() {
+ return &(_current->storage.get());
+ }
+
+ /// const dereference operator.
+ /// Will invoke the operation on the inner T
+ const T* operator->() const {
+ return &(_current->storage.get());
+ }
+
+ /// pre incrementation to the next T in the list
+ Iterator& operator++() {
+ _current = _current->next;
+ return *this;
+ }
+
+ /// post incrementation to the next T in the list
+ Iterator operator++(int) {
+ Iterator tmp(*this);
+ _current = _current->next;
+ return tmp;
+ }
+
+ /// Equality operator
+ friend bool operator==(const Iterator& lhs, const Iterator& rhs) {
+ return lhs._current == rhs._current;
+ }
+
+ /// Unequality operator
+ friend bool operator!=(const Iterator& lhs, const Iterator& rhs) {
+ return !(lhs == rhs);
+ }
+
+ /// return the internal node.
+ Node* get_node() {
+ return _current;
+ }
+
+ private:
+ Node* _current;
+ };
+
+ typedef Iterator iterator;
+
+ /// Construct an empty priority queue.
+ PriorityQueue() : nodes(), free_nodes(NULL), head(NULL), used_nodes_count(0) {
+ initialize();
+ }
+
+ /// Copy construct a priority queue.
+ /// The queue will have the same content has other.
+ PriorityQueue(const PriorityQueue& other) :
+ nodes(), free_nodes(NULL), head(NULL), used_nodes_count(0) {
+ initialize();
+ copy(other);
+ }
+
+ /// destroy a priority queue.
+ ~PriorityQueue() {
+ clear();
+ }
+
+ /// Copy assignemnent from another priority queue.
+ /// The content of the queue will be destroyed then the content from
+ /// other will be copied.
+ PriorityQueue& operator=(const PriorityQueue& other) {
+ if (&other == this) {
+ return *this;
+ }
+ copy(other);
+ return *this;
+ }
+
+ /// Push a new element to the queue.
+ /// It will be added before the first element p in the queue where
+ /// element < p == true.
+ /// @return An iterator to the inserted element.
+ iterator push(const T& element) {
+ if (full()) {
+ return NULL;
+ }
+
+ // get a free node
+ Node* new_node = free_nodes;
+ free_nodes = free_nodes->next;
+ new_node->next = NULL;
+
+ ++used_nodes_count;
+
+ // copy content
+ new (new_node->storage.get_storage()) T(element);
+
+ // if there is no node in the queue, just link the head
+ // to the new node and return
+ if (head == NULL) {
+ head = new_node;
+ return new_node;
+ }
+
+ // if the new node has an higher priority than the node in head
+ // just link it as the head
+ if (element < head->storage.get()) {
+ new_node->next = head;
+ head = new_node;
+ return new_node;
+ }
+
+ // insert the node after head
+ insert_after(head, new_node);
+
+ return new_node;
+ }
+
+ /// pop the head of the queue.
+ bool pop() {
+ if (!head) {
+ return false;
+ }
+
+ Node* target = head;
+ target->storage.get().~T();
+ head = target->next;
+ target->next = free_nodes;
+ free_nodes = target;
+ --used_nodes_count;
+ return true;
+ }
+
+ /// If the content of an element is updated is updated after the insertion
+ /// then, the list can be in an unordered state.
+ /// This function help; it update the position of an iterator in the list.
+ void update(iterator it) {
+ Node* target = it.get_node();
+ Node* hint = head;
+
+ if (target == NULL) {
+ return;
+ }
+
+ // remove the node from the list
+ if (target == head) {
+ // if it is the only node in the list, just return
+ // it is not needed to update its position
+ if (target->next == NULL) {
+ return;
+ }
+
+ // if the order is already correct, just return
+ if (target->storage.get() < target->next->storage.get()) {
+ return;
+ }
+
+ // otherwise remove the node from the list
+ // and update the hint
+ head = target->next;
+ hint = head;
+ } else {
+ bool node_found = false;
+ for (Node* current = head; current != NULL; current = current->next) {
+ if (current->next == target) {
+ // check if it is needed to move the node
+ if (current->storage.get() < target->storage.get()) {
+ if (target->next == NULL) {
+ return;
+ }
+
+ if (target->storage.get() < target->next->storage.get()) {
+ return;
+ }
+
+ // there is no need to iterate again the whole list
+ // just mark the hint has the current node
+ hint = current;
+ }
+
+ // remove the node from the list and break out of the loop
+ current->next = target->next;
+ node_found = true;
+ break;
+ }
+ }
+
+ // the node in parameter doesn't belong to this queue
+ if (!node_found) {
+ return;
+ }
+ }
+
+ // insert the node after hint
+ insert_after(hint, target);
+ }
+
+ /// return an iterator to the begining of the queue.
+ iterator begin() {
+ return head;
+ }
+
+ /// return an iterator to the end of the queue.
+ /// @note can't be dereferenced
+ iterator end() {
+ return NULL;
+ }
+
+ /// erase an iterator from the list
+ bool erase(iterator it) {
+ return erase(it.get_node());
+ }
+
+ /// erase a node from the list
+ bool erase(Node* n) {
+ if (n == NULL) {
+ return false;
+ }
+
+ if (head == n) {
+ return pop();
+ }
+
+ Node* current = head;
+ while (current->next) {
+ if (current->next == n) {
+ current->next = n->next;
+ n->storage.get().~T();
+ n->next = free_nodes;
+ free_nodes = n;
+ --used_nodes_count;
+ return true;
+ }
+ current = current->next;
+ }
+ return false;
+ }
+
+ /**
+ * Indicate if the queue is empty or not.
+ * @return true if the queue is empty and false otherwise.
+ * @invariant the queue remains untouched.
+ */
+ bool empty() const {
+ return head == NULL;
+ }
+
+ /**
+ * Indicate if the true is full or not.
+ * @return true if the queue is full and false otherwise.
+ * @invariant the queue remains untouched.
+ */
+ bool full() const {
+ return free_nodes == NULL;
+ }
+
+ /**
+ * Indicate the number of elements in the queue.
+ * @return the number of elements currently held by the queue.
+ * @invariant the queue remains untouched.
+ */
+ std::size_t size() const {
+ return used_nodes_count;
+ }
+
+ /**
+ * Expose the capacity of the queue in terms of number of elements the
+ * queue can hold.
+ * @return the capacity of the queue.
+ * @invariant this function should always return Capacity.
+ */
+ std::size_t capacity() const {
+ return Capacity;
+ }
+
+ /**
+ * Clear the queue from all its elements.
+ */
+ void clear() {
+ while (head) {
+ head->storage.get().~T();
+ Node* tmp = head;
+ head = head->next;
+ tmp->next = free_nodes;
+ free_nodes = tmp;
+ }
+ used_nodes_count = 0;
+ }
+
+private:
+ void initialize() {
+ /// link all the nodes together
+ for (std::size_t i = 0; i < (Capacity - 1); ++i) {
+ nodes[i].next = &nodes[i + 1];
+ }
+ /// the last node does not have a next node
+ nodes[Capacity - 1].next = NULL;
+ /// set all the nodes as free
+ free_nodes = nodes;
+ }
+
+ void copy(const PriorityQueue& other) {
+ if (empty() == false) {
+ clear();
+ }
+
+ Node *to_copy = other.head;
+ Node *previous = NULL;
+ while (to_copy) {
+ // pick a free node
+ Node* new_node = free_nodes;
+ free_nodes = free_nodes->next;
+ new_node->next = NULL;
+
+ // copy content
+ new (new_node->storage.get_storage()) T(to_copy->storage.get());
+
+ // link into the queue or update head then update previous pointer
+ if (previous) {
+ previous->next = new_node;
+ } else {
+ head = new_node;
+ }
+ previous = new_node;
+
+ // update the node to copy
+ to_copy = to_copy->next;
+ }
+ used_nodes_count = other.used_nodes_count;
+ }
+
+ void insert_after(Node* prev, Node* to_insert) {
+ for (; prev != NULL; prev = prev->next) {
+ if (prev->next == NULL || to_insert->storage.get() < prev->next->storage.get()) {
+ to_insert->next = prev->next;
+ prev->next = to_insert;
+ break;
+ }
+ }
+ }
+
+ Node nodes[Capacity]; //< Nodes of the queue
+ Node *free_nodes; //< entry point for the list of free nodes
+ Node *head; //< head of the queue
+ std::size_t used_nodes_count; // number of nodes used
+};
+
+} // namespace eq
+
+#endif /* EVENTQUEUE_STACKPRIORITYQUEUE_H_ */
diff --git a/implementations/mbed/source/EventQueue/Thunk.h b/implementations/mbed/source/EventQueue/Thunk.h
new file mode 100644
index 0000000..dc6784d
--- /dev/null
+++ b/implementations/mbed/source/EventQueue/Thunk.h
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2016, ARM Limited, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+#ifndef EVENTQUEUE_THUNK_H_
+#define EVENTQUEUE_THUNK_H_
+
+#include "AlignedStorage.h"
+#include "detail/ThunkVTable.h"
+
+namespace eq {
+
+// forward declaration of ThunkVTableGenerator
+namespace detail {
+template
+class ThunkVTableGenerator;
+}
+
+/**
+ * A Thunk is a container holding any kind of nullary callable.
+ * It wrap value semantic and function call operations of the inner callable
+ * held.
+ * \note Thunk of callable bound to arguments should be generated by the
+ * function make_thunk.
+ */
+class Thunk {
+ // Size for the internal buffer of the Thunk
+ static const std::size_t BufferSize = 24;
+
+ template
+ friend class detail::ThunkVTableGenerator;
+
+public:
+
+ /**
+ * Thunk Empty constructor.
+ * When this thunk is called, if does nothing.
+ */
+ Thunk();
+
+ /**
+ * Construct a Thunk from a nullary callable of type F.
+ * When the call operator is invoked, it call a copy of f ( f() ).
+ */
+ template
+ Thunk(const F& f);
+
+ /**
+ * Special constructor for pointer to function.
+ * Allow references to functions to gracefully decay into pointer to function.
+ * Otherwise, reference to function are not copy constructible (their is no
+ * constructible function type in C++).
+ * When the call operator is invoked, it call a copy of f ( f() ).
+ */
+ Thunk(void (*f)());
+
+ /**
+ * Copy construction of a thunk.
+ * Take care that the inner F is correctly copied.
+ */
+ Thunk(const Thunk& other) : _storage(), _vtable() {
+ other._vtable->copy(*this, other);
+ }
+
+ /**
+ * Destruction of the Thunk correctly call the destructor of the
+ * inner callable.
+ */
+ ~Thunk() {
+ _vtable->destroy(*this);
+ }
+
+ /**
+ * Copy assignement from another thunk.
+ * Ensure that the callable held is correctly destroyed then copy
+ * the correctly copy the new one.
+ */
+ Thunk& operator=(const Thunk& other) {
+ if (this == &other) {
+ return *this;
+ }
+ _vtable->destroy(*this);
+ other._vtable->copy(*this, other);
+ return *this;
+ }
+
+ /**
+ * Call operator. Invoke the inner callable.
+ */
+ void operator()() const {
+ _vtable->call(*this);
+ }
+
+private:
+ static void empty_thunk() { }
+
+ AlignedStorage _storage;
+ const detail::ThunkVTable* _vtable;
+};
+
+} // namespace eq
+
+#include "detail/Thunk.impl.h"
+
+#endif /* EVENTQUEUE_THUNK_H_ */
diff --git a/implementations/mbed/source/EventQueue/detail/FunctionAdaptor.h b/implementations/mbed/source/EventQueue/detail/FunctionAdaptor.h
new file mode 100644
index 0000000..61f9da5
--- /dev/null
+++ b/implementations/mbed/source/EventQueue/detail/FunctionAdaptor.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2016, ARM Limited, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+#ifndef EVENTQUEUE_DETAIL_FUNCTIONADAPTOR_H_
+#define EVENTQUEUE_DETAIL_FUNCTIONADAPTOR_H_
+
+#include "MemberFunctionAdaptor.h"
+
+namespace eq {
+namespace detail {
+
+/**
+ * In C++, several types can be used as function:
+ * - function pointer
+ * - member functions
+ * - function like object
+ * While function pointer and function like object can be used with the function
+ * call syntax, the function call syntax can't be applied for function pointers.
+ * This meta function yield takes a callable type F in input and as a result
+ * return a type which can be constructed from F and used with the function call
+ * syntax.
+ *
+ * \code
+ * class Foo;
+ *
+ * Foo foo;
+ * typedef void (Foo::*foo_function_t)();
+ * foo_function_t foo_function = &Foo::some_function;
+ *
+ * //The following will fail:
+ * //foo_function(foo)
+ *
+ * typedef FunctionAdaptor::type foo_function_adaptor_t;
+ * foo_function_adaptor_t foo_function_adapted(foo_function);
+ * foo_function_adapted(foo);
+ *
+ * \endcode
+ *
+ * \tparam F The type of the object to adapt.
+ */
+template
+struct FunctionAdaptor {
+ /**
+ * Common case (function pointer and function like object).
+ * Yield itself, no addaptation needed.
+ */
+ typedef F type;
+};
+
+/**
+ * Partial specializetion for member function with no arguments
+ */
+template
+struct FunctionAdaptor {
+ /**
+ * Yield a member function adaptor.
+ */
+ typedef MemberFunctionAdaptor0 type;
+};
+
+/**
+ * Partial specializetion for member function with one argument
+ */
+template
+struct FunctionAdaptor {
+ /**
+ * Yield a member function adaptor.
+ */
+ typedef MemberFunctionAdaptor1 type;
+};
+
+/**
+ * Partial specializetion for member function with two arguments
+ */
+template
+struct FunctionAdaptor {
+ /**
+ * Yield a member function adaptor.
+ */
+ typedef MemberFunctionAdaptor2 type;
+};
+
+} // namespace detail
+} // namespace eq
+
+#endif /* EVENTQUEUE_DETAIL_FUNCTIONADAPTOR_H_ */
diff --git a/implementations/mbed/source/EventQueue/detail/MemberFunctionAdaptor.h b/implementations/mbed/source/EventQueue/detail/MemberFunctionAdaptor.h
new file mode 100644
index 0000000..9d42c18
--- /dev/null
+++ b/implementations/mbed/source/EventQueue/detail/MemberFunctionAdaptor.h
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2016, ARM Limited, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+#ifndef EVENTQUEUE_DETAIL_MEMBERFUNCTIONADAPTOR_H_
+#define EVENTQUEUE_DETAIL_MEMBERFUNCTIONADAPTOR_H_
+
+namespace eq {
+namespace detail {
+
+/**
+ * Adaptor for member function without argument.
+ * It wrap member function into a function like object to make it usable like
+ * a regular function.
+ * \tparam T the type the class/struct holding the member function.
+ * \code
+ * struct Foo {
+ * void fn();
+ * };
+ *
+ * Foo foo;
+ * MemberFunctionAdaptor0 fn_adapted(&Foo::fn);
+ *
+ * fn_adapted(foo); // work
+ * fn_adapted(&foo); // work
+ * \endcode
+ */
+template
+struct MemberFunctionAdaptor0 {
+ /**
+ * Construct a member function adaptor.
+ * \param fn The member function to addapt.
+ */
+ MemberFunctionAdaptor0(void (T::*fn)()) :
+ _fn(fn) {
+ }
+
+ /**
+ * Call operator for pointer of T
+ */
+ void operator()(T* self) const {
+ (self->*_fn)();
+ }
+
+ /**
+ * Call operator for reference of T
+ */
+ void operator()(T& self) const {
+ (self.*_fn)();
+ }
+
+private:
+ void (T::* const _fn)();
+};
+
+
+/**
+ * Adaptor for member function with one argument.
+ * It wrap member function into a function like object to make it usable like
+ * a regular function.
+ * \tparam T the type the class/struct holding the member function.
+ * \code
+ * struct Foo {
+ * void fn(int);
+ * };
+ *
+ * Foo foo;
+ * MemberFunctionAdaptor1 fn_adapted(&Foo::fn);
+ *
+ * fn_adapted(foo, 42); // work
+ * fn_adapted(&foo, 42); // work
+ * \endcode
+ */
+template
+struct MemberFunctionAdaptor1 {
+ /**
+ * Construct a member function adaptor.
+ * \param fn The member function to addapt.
+ */
+ MemberFunctionAdaptor1(void (T::*fn)(Arg0)) :
+ _fn(fn) {
+ }
+
+ /**
+ * Call operator for pointer of T
+ */
+ void operator()(T* self, Arg0 arg0) const {
+ (self->*_fn)(arg0);
+ }
+
+ /**
+ * Call operator for reference of T
+ */
+ void operator()(T& self, Arg0 arg0) const {
+ (self.*_fn)(arg0);
+ }
+
+private:
+ void (T::* const _fn)(Arg0);
+};
+
+
+/**
+ * Adaptor for member function with two arguments.
+ * It wrap member function into a function like object to make it usable like
+ * a regular function.
+ * \tparam T the type the class/struct holding the member function.
+ * \code
+ * struct Foo {
+ * void fn(int, const char*);
+ * };
+ *
+ * Foo foo;
+ * MemberFunctionAdaptor2 fn_adapted(&Foo::fn);
+ *
+ * fn_adapted(foo, 42, "toto"); // work
+ * fn_adapted(&foo, 42, "toto"); // work
+ * \endcode
+ */
+template
+struct MemberFunctionAdaptor2 {
+ /**
+ * Construct a member function adaptor.
+ * \param fn The member function to addapt.
+ */
+ MemberFunctionAdaptor2(void (T::*fn)(Arg0, Arg1)) : _fn(fn) { }
+
+ /**
+ * Call operator for pointer of T
+ */
+ void operator()(T* self, Arg0 arg0, Arg1 arg1) const {
+ (self->*_fn)(arg0, arg1);
+ }
+
+ /**
+ * Call operator for reference of T
+ */
+ void operator()(T& self, Arg0 arg0, Arg1 arg1) const {
+ (self.*_fn)(arg0, arg1);
+ }
+
+private:
+ void (T::* const _fn)(Arg0, Arg1);
+};
+
+} // namespace detail
+} // namespace eq
+
+#endif /* EVENTQUEUE_DETAIL_MEMBERFUNCTIONADAPTOR_H_ */
diff --git a/implementations/mbed/source/EventQueue/detail/Thunk.impl.h b/implementations/mbed/source/EventQueue/detail/Thunk.impl.h
new file mode 100644
index 0000000..bc515bc
--- /dev/null
+++ b/implementations/mbed/source/EventQueue/detail/Thunk.impl.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2016, ARM Limited, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+#ifndef EVENTQUEUE_DETAIL_THUNK_IMPL_H_
+#define EVENTQUEUE_DETAIL_THUNK_IMPL_H_
+
+#include
+#include "ThunkVTableGenerator.h"
+
+namespace eq {
+
+/**
+ * Thunk constructor Implementation.
+ * Due to the way templates and forwarding work in C++, it was not possible to
+ * provide this implementation in Thunk.h
+ */
+template
+Thunk::Thunk(const F& f) :
+ _storage(),
+ _vtable(&detail::ThunkVTableGenerator::vtable) {
+ typedef __attribute__((unused)) char F_is_too_big_for_the_Thunk[sizeof(F) <= sizeof(_storage) ? 1 : -1];
+ new(_storage.get_storage(0)) F(f);
+}
+
+/**
+ * Specialization for function pointers.
+ * This overload will be chosen when the tyope in input is a reference to a function.
+ * @param f The function to transform in Thunk.
+ */
+inline Thunk::Thunk(void (*f)()) :
+ _storage(),
+ _vtable(&detail::ThunkVTableGenerator::vtable) {
+ typedef void(*F)();
+ typedef __attribute__((unused)) char F_is_too_big_for_the_Thunk[sizeof(F) <= sizeof(_storage) ? 1 : -1];
+ new(_storage.get_storage(0)) F(f);
+}
+
+/**
+ * Thunk empty constructor Implementation.
+ * Due to the way templates and forwarding work in C++, it was not possible to
+ * provide this implementation in Thunk.h
+ */
+inline Thunk::Thunk() :
+ _storage(),
+ _vtable(&detail::ThunkVTableGenerator::vtable) {
+ typedef void(*F)();
+ typedef __attribute__((unused)) char F_is_too_big_for_the_Thunk[sizeof(F) <= sizeof(_storage) ? 1 : -1];
+ new(_storage.get_storage(0)) F(empty_thunk);
+}
+
+} // namespace eq
+
+#endif /* EVENTQUEUE_DETAIL_THUNK_IMPL_H_ */
diff --git a/implementations/mbed/source/EventQueue/detail/ThunkVTable.h b/implementations/mbed/source/EventQueue/detail/ThunkVTable.h
new file mode 100644
index 0000000..65dcf55
--- /dev/null
+++ b/implementations/mbed/source/EventQueue/detail/ThunkVTable.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2016, ARM Limited, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+#ifndef EVENTQUEUE_DETAIL_THUNKVTABLE_H_
+#define EVENTQUEUE_DETAIL_THUNKVTABLE_H_
+
+namespace eq {
+
+// forward declaration of the Thunk class
+class Thunk;
+
+namespace detail {
+
+/**
+ * This POD is used as a vtable by Thunk implementation.
+ * Thunk is a value type for all type nullary callable and therefore standard
+ * polymorphism is not suitable for that use case.
+ * Instead, the vtable is generated for each type contained in a thunk.
+ * This structure is the prototype of such vtable.
+ * \note see ThunkVTableGenerator for implementation and the generation of
+ * Thunk vtables.
+ */
+struct ThunkVTable {
+ typedef Thunk thunk_t;
+
+ /**
+ * destroy a thunk (act like a destructor).
+ */
+ void (* const destroy)(thunk_t& self);
+
+ /**
+ * Copy self into dest.
+ * It is expected that dest is empty.
+ */
+ void (* const copy)(thunk_t& dest, const thunk_t& self);
+
+ /**
+ * Synthetized call for the inner object of the thunk_t.
+ */
+ void (* const call)(const thunk_t& self);
+};
+
+} // namespace detail
+} // namespace eq
+
+#endif /* EVENTQUEUE_DETAIL_THUNKVTABLE_H_ */
diff --git a/implementations/mbed/source/EventQueue/detail/ThunkVTableGenerator.h b/implementations/mbed/source/EventQueue/detail/ThunkVTableGenerator.h
new file mode 100644
index 0000000..bdbb117
--- /dev/null
+++ b/implementations/mbed/source/EventQueue/detail/ThunkVTableGenerator.h
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2016, ARM Limited, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+#ifndef EVENTQUEUE_DETAIL_THUNKVTABLEGENERATOR_H_
+#define EVENTQUEUE_DETAIL_THUNKVTABLEGENERATOR_H_
+
+// imported from Thunk.h
+
+namespace eq {
+namespace detail {
+
+/**
+ * Thunk VTable Generator.
+ * This class generate the vtable of a type F for a Thunk.
+ * \tparam F The type of the callable for which the Thunk vtable should be
+ * generated.
+ */
+template
+struct ThunkVTableGenerator {
+ typedef Thunk thunk_t;
+
+ /**
+ * Implementation of destructor for Thunk holding an F.
+ * @param self The thunk to destroy
+ */
+ static void destroy(thunk_t& self) {
+ get_ptr(self)->~F();
+ }
+
+ /**
+ * Implementation of copy (used by copy constructor and copy assignment)
+ * for a Thunk holding an F.
+ * @param dest The thunk receiving the copy.
+ * @param self The thunk to copy.
+ */
+ static void copy(thunk_t& dest, const thunk_t& self) {
+ new (get_ptr(dest)) F(*get_ptr(self));
+ dest._vtable = self._vtable;
+ }
+
+ /**
+ * Implementation of call operator for a Thunk holding an F.
+ * @param self The thunk containing the F to call.
+ */
+ static void call(const thunk_t& self) {
+ (*get_ptr(self))();
+ }
+
+ /**
+ * The Thunk vtable for an F.
+ */
+ static const ThunkVTable vtable;
+
+private:
+ /**
+ * Accessor to the pointer to F contained in the Thunk.
+ */
+ static F* get_ptr(thunk_t& thunk) {
+ return static_cast(thunk._storage.get_storage(0));
+ }
+
+ /**
+ * Accessor to the const pointer to F contained in the const Thunk.
+ */
+ static const F* get_ptr(const thunk_t& thunk) {
+ return static_cast(thunk._storage.get_storage(0));
+ }
+};
+
+/**
+ * Instantiation of the Thunk vtable of F.
+ */
+template
+const ThunkVTable ThunkVTableGenerator::vtable = {
+ ThunkVTableGenerator::destroy,
+ ThunkVTableGenerator::copy,
+ ThunkVTableGenerator::call
+};
+
+} // namespace detail
+} // namespace eq
+
+#endif /* EVENTQUEUE_DETAIL_THUNKVTABLEGENERATOR_H_ */
diff --git a/implementations/mbed/source/EventQueue/detail/Thunks.h b/implementations/mbed/source/EventQueue/detail/Thunks.h
new file mode 100644
index 0000000..39e6822
--- /dev/null
+++ b/implementations/mbed/source/EventQueue/detail/Thunks.h
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2016, ARM Limited, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+#ifndef EVENTQUEUE_DETAIL_THUNKS_H_
+#define EVENTQUEUE_DETAIL_THUNKS_H_
+
+namespace eq {
+namespace detail {
+
+/**
+ * Generate a Thunk for a callable of type F with one argument.
+ * This class is a function like object containing the function to call and
+ * its argument. When it is invoked, F is invoked with the argument passed
+ * at construction time.
+ * \tparam F the type of the callable.
+ * \tparam Arg0 type of the first parameter of F to pass to F.
+ */
+template
+struct Thunk_1 {
+ /**
+ * Construct the Thunk and bind its arguments.
+ * \param fn the callable, it will be invoked with arg0.
+ * \param arg0 The first argument to pass to fn when this object is called.
+ * \note member function should be adapted by using FunctionAdaptor
+ */
+ Thunk_1(const F& fn, const Arg0& arg0) :
+ _fn(fn), _arg0(arg0) {
+ }
+
+ /**
+ * Apply arg0 to fn.
+ */
+ void operator()() const {
+ _fn(_arg0);
+ }
+
+private:
+ mutable F _fn;
+ mutable Arg0 _arg0;
+};
+
+/**
+ * Generate a Thunk for a callable of type F with two arguments.
+ * This class is a function like object containing the function to call and
+ * its arguments. When it is invoked, F is invoked with the arguments passed
+ * at construction time.
+ * \tparam F the type of the callable.
+ * \tparam Arg0 type of the first parameter to pass to F.
+ * \tparam Arg1 type of the second parameter to pass to F.
+ */
+template
+struct Thunk_2 {
+ /**
+ * Construct the Thunk and bind its arguments.
+ * \param fn the callable, it will be invoked with arg0 and arg1.
+ * \param arg0 The first argument to pass to fn when this object is called.
+ * \param arg1 The second argument to pass to fn when this object is called.
+ * \note member function should be adapted by using FunctionAdaptor
+ */
+ Thunk_2(const F& fn, const Arg0& arg0, const Arg1& arg1) :
+ _fn(fn),
+ _arg0(arg0),
+ _arg1(arg1) {
+ }
+
+ /**
+ * Apply arg0 and arg1 to fn.
+ */
+ void operator()() const {
+ _fn(_arg0, _arg1);
+ }
+
+private:
+ mutable F _fn;
+ mutable Arg0 _arg0;
+ mutable Arg1 _arg1;
+};
+
+/**
+ * Generate a Thunk for a callable of type F with three arguments.
+ * This class is a function like object containing the function to call and
+ * its arguments. When it is invoked, F is invoked with the arguments passed
+ * at construction time.
+ * \tparam F the type of the callable.
+ * \tparam Arg0 type of the first parameter to pass to F.
+ * \tparam Arg1 type of the second parameter to pass to F.
+ * \tparam Arg2 type of the third parameter to pass to F.
+ */
+template
+struct Thunk_3 {
+ /**
+ * Construct the Thunk and bind its arguments.
+ * \param fn the callable, it will be invoked with arg0, arg1 and arg2.
+ * \param arg0 The first argument to pass to fn when this object is called.
+ * \param arg1 The second argument to pass to fn when this object is called.
+ * \param arg2 The third argument to pass to fn when this object is called.
+ * \note member function should be adapted by using FunctionAdaptor
+ */
+ Thunk_3(const F& fn, const Arg0& arg0, const Arg1& arg1, const Arg2& arg2) :
+ _fn(fn),
+ _arg0(arg0),
+ _arg1(arg1),
+ _arg2(arg2){
+ }
+
+ /**
+ * Apply arg0, arg1 and arg2 to fn.
+ */
+ void operator()() const {
+ _fn(_arg0, _arg1, _arg2);
+ }
+
+private:
+ mutable F _fn;
+ mutable Arg0 _arg0;
+ mutable Arg1 _arg1;
+ mutable Arg2 _arg2;
+};
+
+} // namespace detail
+} // namespace eq
+
+#endif /* EVENTQUEUE_DETAIL_THUNKS_H_ */
diff --git a/implementations/mbed/source/EventQueue/util/CriticalSectionLock.h b/implementations/mbed/source/EventQueue/util/CriticalSectionLock.h
new file mode 100644
index 0000000..792b7bb
--- /dev/null
+++ b/implementations/mbed/source/EventQueue/util/CriticalSectionLock.h
@@ -0,0 +1,59 @@
+/*
+ * PackageLicenseDeclared: Apache-2.0
+ * Copyright (c) 2015 ARM Limited
+ *
+ * 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.
+ */
+
+#ifndef __MBED_UTIL_CRITICAL_SECTION_LOCK_H__
+#define __MBED_UTIL_CRITICAL_SECTION_LOCK_H__
+
+#include
+#include "cmsis.h"
+
+namespace mbed {
+namespace util {
+
+/** RAII object for disabling, then restoring, interrupt state
+ * Usage:
+ * @code
+ *
+ * void f() {
+ * // some code here
+ * {
+ * CriticalSectionLock lock;
+ * // Code in this block will run with interrupts disabled
+ * }
+ * // interrupts will be restored to their previous state
+ * }
+ * @endcode
+ */
+class CriticalSectionLock {
+public:
+ CriticalSectionLock() {
+ _state = __get_PRIMASK();
+ __disable_irq();
+ }
+
+ ~CriticalSectionLock() {
+ __set_PRIMASK(_state);
+ }
+
+private:
+ uint32_t _state;
+};
+
+} // namespace util
+} // namespace mbed
+
+#endif // #ifndef __MBED_UTIL_CRITICAL_SECTION_LOCK_H__
diff --git a/implementations/mbed/source/PersistentStorageHelper/ConfigParamsPersistence.cpp b/implementations/mbed/source/PersistentStorageHelper/ConfigParamsPersistence.cpp
new file mode 100644
index 0000000..f5a369d
--- /dev/null
+++ b/implementations/mbed/source/PersistentStorageHelper/ConfigParamsPersistence.cpp
@@ -0,0 +1,55 @@
+/* mbed Microcontroller Library
+ * Copyright (c) 2006-2015 ARM Limited
+ *
+ * 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.
+ */
+
+#include "ConfigParamsPersistence.h"
+
+#if !defined(TARGET_NRF51822) && !defined(TARGET_NRF52832) /* Persistent storage supported on nrf51 platforms */
+ /**
+ * When not using an nRF51-based target then persistent storage is not available.
+ */
+ #warning "EddystoneService is not configured to store configuration data in non-volatile memory"
+
+ bool loadEddystoneServiceConfigParams(EddystoneService::EddystoneParams_t *paramsP)
+ {
+ /* Avoid compiler warnings */
+ (void) paramsP;
+
+ /*
+ * Do nothing and let the main program set Eddystone params to
+ * defaults
+ */
+ return false;
+ }
+
+ void saveEddystoneServiceConfigParams(const EddystoneService::EddystoneParams_t *paramsP)
+ {
+ /* Avoid compiler warnings */
+ (void) paramsP;
+
+ /* Do nothing... */
+ return;
+ }
+
+ void saveEddystoneTimeParams(const TimeParams_t *timeP)
+ {
+ /* Avoid compiler warnings */
+ (void) timeP;
+
+ /* Do nothing... */
+ return;
+ }
+
+#endif /* #ifdef TARGET_NRF51822 */
diff --git a/implementations/mbed/source/PersistentStorageHelper/ConfigParamsPersistence.h b/implementations/mbed/source/PersistentStorageHelper/ConfigParamsPersistence.h
new file mode 100644
index 0000000..05c99a2
--- /dev/null
+++ b/implementations/mbed/source/PersistentStorageHelper/ConfigParamsPersistence.h
@@ -0,0 +1,67 @@
+/* mbed Microcontroller Library
+ * Copyright (c) 2006-2015 ARM Limited
+ *
+ * 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.
+ */
+
+#ifndef __BLE_CONFIG_PARAMS_PERSISTENCE_H__
+#define __BLE_CONFIG_PARAMS_PERSISTENCE_H__
+
+#include "../EddystoneService.h"
+
+/**
+ * Generic API to load the Eddystone Service configuration parameters from persistent
+ * storage. If persistent storage isn't available, the persistenceSignature
+ * member of params may be left un-initialized to the MAGIC, and this will cause
+ * a reset to default values.
+ *
+ * @param[out] paramsP
+ * The parameters to be filled in from persistence storage. This
+ * argument can be NULL if the caller is only interested in
+ * discovering the persistence status of params.
+ *
+ * @return true if params were loaded from persistent storage and have usefully
+ * initialized fields.
+ */
+bool loadEddystoneServiceConfigParams(EddystoneService::EddystoneParams_t *paramsP);
+
+/**
+ * Generic API to store the Eddystone Service configuration parameters to persistent
+ * storage. It typically initializes the persistenceSignature member of the
+ * params to the MAGIC value to indicate persistence.
+ *
+ * @param[in,out] paramsP
+ * The params to be saved; persistenceSignature member gets
+ * updated if persistence is successful.
+ *
+ * @note The save operation may be asynchronous. It may be a short while before
+ * the request takes affect. Reading back saved configParams may not yield
+ * correct behaviour if attempted soon after a store.
+ */
+void saveEddystoneServiceConfigParams(const EddystoneService::EddystoneParams_t *paramsP);
+
+/**
+ * Generic API to store the Eddystone TimeParams (a subset of Config Params) for
+ * speed/power efficiency.
+ *
+ * @param[in,out] timeP
+ * The params to be saved; persistenceSignature member gets
+ * updated if persistence is successful.
+ *
+ * @note The save operation may be asynchronous. It may be a short while before
+ * the request takes affect. Reading back saved configParams may not yield
+ * correct behaviour if attempted soon after a store.
+ */
+void saveEddystoneTimeParams(const TimeParams_t *timeP);
+
+#endif /* #ifndef __BLE_CONFIG_PARAMS_PERSISTENCE_H__*/
diff --git a/implementations/mbed/source/PersistentStorageHelper/nrfPersistentStorageHelper/nrfConfigParamsPersistence.cpp b/implementations/mbed/source/PersistentStorageHelper/nrfPersistentStorageHelper/nrfConfigParamsPersistence.cpp
new file mode 100644
index 0000000..ef1c977
--- /dev/null
+++ b/implementations/mbed/source/PersistentStorageHelper/nrfPersistentStorageHelper/nrfConfigParamsPersistence.cpp
@@ -0,0 +1,140 @@
+/* mbed Microcontroller Library
+ * Copyright (c) 2006-2015 ARM Limited
+ *
+ * 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.
+ */
+
+#if defined(TARGET_NRF51822) || defined(TARGET_NRF52832) /* Persistent storage supported on nrf51 platforms */
+
+extern "C" {
+ #include "pstorage.h"
+}
+
+#include "nrf_error.h"
+#include "../../EddystoneService.h"
+#include
+
+/**
+ * Nordic specific structure used to store params persistently.
+ * It extends EddystoneService::EddystoneParams_t with a persistence signature.
+ */
+struct PersistentParams_t {
+ EddystoneService::EddystoneParams_t params;
+ uint32_t persistenceSignature; /* This isn't really a parameter, but having the expected
+ * magic value in this field indicates persistence. */
+
+ static const uint32_t MAGIC = 0x1BEAC000; /* Magic that identifies persistence */
+};
+
+/**
+ * The following is a module-local variable to hold configuration parameters for
+ * short periods during flash access. This is necessary because the pstorage
+ * APIs don't copy in the memory provided as data source. The memory cannot be
+ * freed or reused by the application until this flash access is complete. The
+ * load and store operations in this module initialize persistentParams and then
+ * pass it on to the 'pstorage' APIs.
+ */
+static PersistentParams_t persistentParams;
+
+static pstorage_handle_t pstorageHandle;
+
+/**
+ * Dummy callback handler needed by Nordic's pstorage module. This is called
+ * after every flash access.
+ */
+static void pstorageNotificationCallback(pstorage_handle_t *p_handle,
+ uint8_t op_code,
+ uint32_t result,
+ uint8_t *p_data,
+ uint32_t data_len)
+{
+ /* Supress compiler warnings */
+ (void) p_handle;
+ (void) op_code;
+ (void) result;
+ (void) p_data;
+ (void) data_len;
+
+ /* APP_ERROR_CHECK(result); */
+}
+
+/* Platform-specific implementation for persistence on the nRF5x. Based on the
+ * pstorage module provided by the Nordic SDK. */
+bool loadEddystoneServiceConfigParams(EddystoneService::EddystoneParams_t *paramsP)
+{
+ static bool pstorageInitied = false;
+ if (!pstorageInitied) {
+ pstorage_init();
+
+ static pstorage_module_param_t pstorageParams = {
+ .cb = pstorageNotificationCallback,
+ .block_size = sizeof(PersistentParams_t),
+ .block_count = 1
+ };
+ pstorage_register(&pstorageParams, &pstorageHandle);
+ pstorageInitied = true;
+ }
+
+ if ((pstorage_load(reinterpret_cast(&persistentParams), &pstorageHandle, sizeof(PersistentParams_t), 0) != NRF_SUCCESS) ||
+ (persistentParams.persistenceSignature != PersistentParams_t::MAGIC)) {
+ // On failure zero out and let the service reset to defaults
+ memset(paramsP, 0, sizeof(EddystoneService::EddystoneParams_t));
+ return false;
+ }
+
+ memcpy(paramsP, &persistentParams.params, sizeof(EddystoneService::EddystoneParams_t));
+ return true;
+}
+
+/* Platform-specific implementation for persistence on the nRF5x. Based on the
+ * pstorage module provided by the Nordic SDK. */
+void saveEddystoneServiceConfigParams(const EddystoneService::EddystoneParams_t *paramsP)
+{
+ memcpy(&persistentParams.params, paramsP, sizeof(EddystoneService::EddystoneParams_t));
+ if (persistentParams.persistenceSignature != PersistentParams_t::MAGIC) {
+ persistentParams.persistenceSignature = PersistentParams_t::MAGIC;
+ pstorage_store(&pstorageHandle,
+ reinterpret_cast(&persistentParams),
+ sizeof(PersistentParams_t),
+ 0 /* offset */);
+ } else {
+ pstorage_update(&pstorageHandle,
+ reinterpret_cast(&persistentParams),
+ sizeof(PersistentParams_t),
+ 0 /* offset */);
+ }
+}
+
+/* Saves only the TimeParams (a subset of Config Params) for speed/power efficiency
+ * Platform-specific implementation for persistence on the nRF5x. Based on the
+ * pstorage module provided by the Nordic SDK. */
+void saveEddystoneTimeParams(const TimeParams_t *timeP)
+{
+ // Copy the time params object to the main datastructure
+ memcpy(&persistentParams.params.timeParams, timeP, sizeof(TimeParams_t));
+ // Test if this is the first pstorage update, or an update
+ if (persistentParams.persistenceSignature != PersistentParams_t::MAGIC) {
+ persistentParams.persistenceSignature = PersistentParams_t::MAGIC;
+ pstorage_store(&pstorageHandle,
+ reinterpret_cast(&persistentParams),
+ sizeof(TimeParams_t),
+ offsetof(PersistentParams_t, params.timeParams) /* offset */);
+ } else {
+ pstorage_update(&pstorageHandle,
+ reinterpret_cast(&persistentParams),
+ sizeof(TimeParams_t),
+ offsetof(PersistentParams_t, params.timeParams) /* offset */);
+ }
+}
+
+#endif /* #ifdef TARGET_NRF51822 */
diff --git a/implementations/mbed/source/TLMFrame.cpp b/implementations/mbed/source/TLMFrame.cpp
new file mode 100644
index 0000000..6df29ac
--- /dev/null
+++ b/implementations/mbed/source/TLMFrame.cpp
@@ -0,0 +1,196 @@
+/*
+ * Copyright (c) 2006-2016 Google Inc, All Rights Reserved
+ *
+ * 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.
+ */
+
+#include "TLMFrame.h"
+#include "EddystoneService.h"
+
+TLMFrame::TLMFrame(uint8_t tlmVersionIn,
+ uint16_t tlmBatteryVoltageIn,
+ uint16_t tlmBeaconTemperatureIn,
+ uint32_t tlmPduCountIn,
+ uint32_t tlmTimeSinceBootIn) :
+ tlmVersion(tlmVersionIn),
+ lastTimeSinceBootRead(0),
+ tlmBatteryVoltage(tlmBatteryVoltageIn),
+ tlmBeaconTemperature(tlmBeaconTemperatureIn),
+ tlmPduCount(tlmPduCountIn),
+ tlmTimeSinceBoot(tlmTimeSinceBootIn)
+{
+}
+
+void TLMFrame::setTLMData(uint8_t tlmVersionIn)
+{
+ /* According to the Eddystone spec BatteryVoltage is 0 and
+ * BeaconTemperature is 0x8000 if not supported
+ */
+ tlmVersion = tlmVersionIn;
+ tlmBatteryVoltage = 0;
+ tlmBeaconTemperature = 0x8000;
+ tlmPduCount = 0;
+ tlmTimeSinceBoot = 0;
+}
+
+void TLMFrame::setData(uint8_t *rawFrame) // add eidTime - a 4 byte quantity
+{
+ size_t index = 0;
+ rawFrame[index++] = EDDYSTONE_UUID_SIZE + FRAME_SIZE_TLM; // Length of Frame
+ rawFrame[index++] = EDDYSTONE_UUID[0]; // 16-bit Eddystone UUID
+ rawFrame[index++] = EDDYSTONE_UUID[1];
+ rawFrame[index++] = FRAME_TYPE_TLM; // Eddystone frame type = Telemetry
+ rawFrame[index++] = tlmVersion; // TLM Version Number
+ rawFrame[index++] = (uint8_t)(tlmBatteryVoltage >> 8); // Battery Voltage[0]
+ rawFrame[index++] = (uint8_t)(tlmBatteryVoltage >> 0); // Battery Voltage[1]
+ rawFrame[index++] = (uint8_t)(tlmBeaconTemperature >> 8); // Beacon Temp[0]
+ rawFrame[index++] = (uint8_t)(tlmBeaconTemperature >> 0); // Beacon Temp[1]
+ rawFrame[index++] = (uint8_t)(tlmPduCount >> 24); // PDU Count [0]
+ rawFrame[index++] = (uint8_t)(tlmPduCount >> 16); // PDU Count [1]
+ rawFrame[index++] = (uint8_t)(tlmPduCount >> 8); // PDU Count [2]
+ rawFrame[index++] = (uint8_t)(tlmPduCount >> 0); // PDU Count [3]
+ rawFrame[index++] = (uint8_t)(tlmTimeSinceBoot >> 24); // Time Since Boot [0]
+ rawFrame[index++] = (uint8_t)(tlmTimeSinceBoot >> 16); // Time Since Boot [1]
+ rawFrame[index++] = (uint8_t)(tlmTimeSinceBoot >> 8); // Time Since Boot [2]
+ rawFrame[index++] = (uint8_t)(tlmTimeSinceBoot >> 0); // Time Since Boot [3]
+}
+
+void TLMFrame::encryptData(uint8_t* rawFrame, uint8_t* eidIdentityKey, uint8_t rotationPeriodExp, uint32_t beaconTimeSecs) {
+ // Initialize AES data
+ mbedtls_aes_context ctx;
+ mbedtls_aes_init(&ctx);
+ mbedtls_aes_setkey_enc(&ctx, eidIdentityKey, sizeof(EidIdentityKey_t) *8 );
+ // Change the TLM version number to the encrypted version
+ rawFrame[VERSION_OFFSET] = ETLM_VERSION; // Encrypted TLM Version number
+ // Create EAX Params
+ uint8_t nonce[ETLM_NONCE_LEN];
+ // Calculate the 48-bit nonce
+ generateEtlmNonce(nonce, rotationPeriodExp, beaconTimeSecs);
+
+ uint8_t* input = rawFrame + DATA_OFFSET; // array size 12
+ uint8_t output[ETLM_DATA_LEN]; // array size 16 (4 bytes are added: SALT[2], MIC[2])
+ memset(output, 0, ETLM_DATA_LEN);
+ uint8_t emptyHeader[1]; // Empty header
+ LOG(("EIDIdentityKey=\r\n")); EddystoneService::logPrintHex(eidIdentityKey, 16);
+ LOG(("ETLM Encoder INPUT=\r\n")); EddystoneService::logPrintHex(input, 12);
+ LOG(("ETLM SALT=\r\n")); EddystoneService::logPrintHex(nonce+4, 2);
+ LOG(("ETLM Nonce=\r\n")); EddystoneService::logPrintHex(nonce, 6);
+ // Encrypt the TLM to ETLM
+ eddy_aes_authcrypt_eax(&ctx, MBEDTLS_AES_ENCRYPT, nonce, sizeof(nonce), emptyHeader, 0, TLM_DATA_LEN, input, output, output + MIC_OFFSET, MIC_LEN);
+
+#ifndef NO_EAX_TEST
+ // Part of test code to confirm x == EAX_DECRYPT( EAX_ENCRYPT(x) )
+ uint8_t newinput[ETLM_DATA_LEN];
+ memcpy(newinput, output, ETLM_DATA_LEN);
+#endif
+
+ // Only use first 2 bytes of Nonce
+ output[SALT_OFFSET] = nonce[4]; // Nonce MSB
+ output[SALT_OFFSET+1] = nonce[5]; // Nonce LSB
+ LOG(("ETLM output+SALT=\r\n")); EddystoneService::logPrintHex(output, 16);
+ // copy the encrypted payload to the output
+ memcpy((rawFrame + DATA_OFFSET), output, ETLM_DATA_LEN);
+
+#ifndef NO_EAX_TEST
+ // Perform test to confirm x == EAX_DECRYPT( EAX_ENCRYPT(x) )
+ uint8_t buf[ETLM_DATA_LEN];
+ memset(buf, 0, ETLM_DATA_LEN);
+ int ret = eddy_aes_authcrypt_eax(&ctx, MBEDTLS_AES_DECRYPT, nonce, sizeof(nonce), emptyHeader, 0, TLM_DATA_LEN, newinput, buf, newinput + MIC_OFFSET, MIC_LEN);
+ LOG(("ETLM Decoder OUTPUT ret=%d buf=\r\n", ret)); EddystoneService::logPrintHex(buf, 12);
+#endif
+
+ // fix the frame length to the encrypted length
+ rawFrame[FRAME_LEN_OFFSET] = FRAME_SIZE_ETLM + EDDYSTONE_UUID_SIZE;
+ // Free the AES data struture
+ mbedtls_aes_free(&ctx);
+}
+
+
+size_t TLMFrame::getRawFrameSize(uint8_t* rawFrame)
+{
+ return rawFrame[FRAME_LEN_OFFSET];
+}
+
+uint8_t* TLMFrame::getData(uint8_t* rawFrame)
+{
+ if (rawFrame[VERSION_OFFSET] == TLM_VERSION) {
+ setData(rawFrame);
+ }
+ return &(rawFrame[TLM_DATA_OFFSET]);
+}
+
+uint8_t TLMFrame::getDataLength(uint8_t* rawFrame)
+{
+ return rawFrame[FRAME_LEN_OFFSET] - EDDYSTONE_UUID_LEN;
+}
+
+uint8_t* TLMFrame::getAdvFrame(uint8_t* rawFrame){
+ return &(rawFrame[ADV_FRAME_OFFSET]);
+}
+
+uint8_t TLMFrame::getAdvFrameLength(uint8_t* rawFrame){
+ return rawFrame[FRAME_LEN_OFFSET];
+}
+
+void TLMFrame::updateTimeSinceLastBoot(uint32_t nowInMillis)
+{
+ // Measured in tenths of a second
+ tlmTimeSinceBoot += (nowInMillis - lastTimeSinceBootRead) / 100;
+ lastTimeSinceBootRead = nowInMillis;
+}
+
+void TLMFrame::updateBatteryVoltage(uint16_t tlmBatteryVoltageIn)
+{
+ tlmBatteryVoltage = tlmBatteryVoltageIn;
+}
+
+void TLMFrame::updateBeaconTemperature(uint16_t tlmBeaconTemperatureIn)
+{
+ tlmBeaconTemperature = tlmBeaconTemperatureIn;
+}
+
+void TLMFrame::updatePduCount(void)
+{
+ tlmPduCount++;
+}
+
+uint16_t TLMFrame::getBatteryVoltage(void) const
+{
+ return tlmBatteryVoltage;
+}
+
+uint16_t TLMFrame::getBeaconTemperature(void) const
+{
+ return tlmBeaconTemperature;
+}
+
+uint8_t TLMFrame::getTLMVersion(void) const
+{
+ return tlmVersion;
+}
+
+int TLMFrame::generateEtlmNonce(uint8_t* nonce, uint8_t rotationPeriodExp, uint32_t beaconTimeSecs) {
+ int rc = 0;
+ if (sizeof(nonce) != ETLM_NONCE_LEN) {
+ rc = ETLM_NONCE_INVALID_LEN;
+ }
+ uint32_t scaledTime = (beaconTimeSecs >> rotationPeriodExp) << rotationPeriodExp;
+ int index = 0;
+ nonce[index++] = (scaledTime >> 24) & 0xff;
+ nonce[index++] = (scaledTime >> 16) & 0xff;
+ nonce[index++] = (scaledTime >> 8) & 0xff;
+ nonce[index++] = scaledTime & 0xff;
+ EddystoneService::generateRandom(nonce + index, SALT_LEN);
+ return rc;
+}
+
diff --git a/implementations/mbed/source/TLMFrame.h b/implementations/mbed/source/TLMFrame.h
new file mode 100644
index 0000000..9bcc6e1
--- /dev/null
+++ b/implementations/mbed/source/TLMFrame.h
@@ -0,0 +1,276 @@
+/*
+ * Copyright (c) 2006-2016 Google Inc, All Rights Reserved
+ *
+ * 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.
+ */
+
+#ifndef __TLMFRAME_H__
+#define __TLMFRAME_H__
+
+#include "EddystoneTypes.h"
+#include "aes_eax.h"
+
+/**
+ * Class that encapsulates data that belongs to the Eddystone-TLM frame. For
+ * more information refer to https://github.com/google/eddystone/tree/master/eddystone-tlm.
+ */
+class TLMFrame
+{
+public:
+ /**
+ * Construct a new instance of this class.
+ *
+ * @param[in] tlmVersionIn
+ * Eddystone-TLM version number to use.
+ * @param[in] tlmBatteryVoltageIn
+ * Initial value for the Eddystone-TLM Battery Voltage.
+ * @param[in] tlmBeaconTemperatureIn
+ * Initial value for the Eddystone-TLM Beacon Temperature.
+ * @param[in] tlmPduCountIn
+ * Initial value for the Eddystone-TLM Advertising PDU Count.
+ * @param[in] tlmTimeSinceBootIn
+ * Intitial value for the Eddystone-TLM time since boot timer.
+ 8 This timer has a 0.1 second resolution.
+ */
+ TLMFrame(uint8_t tlmVersionIn = 0,
+ uint16_t tlmBatteryVoltageIn = 0,
+ uint16_t tlmBeaconTemperatureIn = 0x8000,
+ uint32_t tlmPduCountIn = 0,
+ uint32_t tlmTimeSinceBootIn = 0);
+
+ /**
+ * Set the Eddystone-TLM version number.
+ */
+ void setTLMData(uint8_t tlmVersionIn = 0);
+
+ /**
+ * Construct the raw bytes of the Eddystone-TLM frame that will be directly
+ * used in the advertising packets.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ */
+ void setData(uint8_t *rawFrame);
+
+ /**
+ * Construct the encrypted bytes of the Eddystone-ETLM frame that will be directly
+ * used in the advertising packets.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ * @param[in] eidIdentityKey
+ * Pointer to the eidIdentityKey in use
+ * @param[in] rotationPeriodExp
+ * Rotation exponent for EID
+ * @param[in] beaconTimeSecs
+ * Time in seconds since beacon boot.
+ */
+ void encryptData(uint8_t* rawFrame, uint8_t* eidIdentityKey, uint8_t rotationPeriodExp, uint32_t beaconTimeSecs);
+
+ /**
+ * Get the size of the Eddystone-TLM frame constructed with the
+ * current state of the TLMFrame object.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return The size in bytes of the Eddystone-TLM frame.
+ */
+ size_t getRawFrameSize(uint8_t* rawFrame);
+
+
+ /**
+ * Get the TLM frame data from the Eddystone-TLM frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return A pointer to the bytes of the Eddystone-TLM frame data.
+ */
+ uint8_t* getData(uint8_t* rawFrame);
+
+ /**
+ * Get the length of the TLM frame data from the Eddystone-TLM frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return The size in bytes of the Eddystone-TLM frame.
+ */
+ uint8_t getDataLength(uint8_t* rawFrame);
+
+ /**
+ * Get the TLM Adv data from the Eddystone-TLMframe.
+ * This is the full service data included in the BLE service data params
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return A pointer to the bytes of the Eddystone-TLM Adv frame data.
+ */
+ uint8_t* getAdvFrame(uint8_t* rawFrame);
+
+ /**
+ * Get the length of the TLM Adv data from the Eddystone-TLMframe.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return The size in bytes of the Eddystone-TLM Adv frame data.
+ */
+ uint8_t getAdvFrameLength(uint8_t* rawFrame);
+
+ /**
+ * Update the time since last boot.
+ *
+ * @param[in] nowInMillis
+ * The time since boot in milliseconds.
+ */
+ void updateTimeSinceLastBoot(uint32_t nowInMillis);
+
+ /**
+ * Update the Battery Voltage.
+ *
+ * @param[in] tlmBatteryVoltageIn
+ * The new Battery Voltage value.
+ */
+ void updateBatteryVoltage(uint16_t tlmBatteryVoltageIn);
+
+ /**
+ * Update the Beacon Temperature.
+ *
+ * @param[in] tlmBeaconTemperatureIn
+ * The new Beacon Temperature value.
+ */
+ void updateBeaconTemperature(uint16_t tlmBeaconTemperatureIn);
+
+ /**
+ * Increment the current PDU counter by 1.
+ */
+ void updatePduCount(void);
+
+ /**
+ * Get the current Battery Voltage.
+ *
+ * @return The Battery Voltage.
+ */
+ uint16_t getBatteryVoltage(void) const;
+
+ /**
+ * Get the current Beacon Temperature.
+ *
+ * @return The Beacon Temperature.
+ */
+ uint16_t getBeaconTemperature(void) const;
+
+ /**
+ * Get the current TLM Version number.
+ *
+ * @return The TLM Version number.
+ */
+ uint8_t getTLMVersion(void) const;
+
+ /**
+ * The byte ID of an Eddystone-TLM frame.
+ */
+ static const uint8_t FRAME_TYPE_TLM = 0x20;
+
+ /**
+ * The verison number of the Telemetry packets being used
+ */
+ static const uint8_t DEFAULT_TLM_VERSION = 0;
+
+ /**
+ * The size of an Eddystone-TLM frame.
+ */
+ static const uint8_t FRAME_SIZE_TLM = 14;
+ /**
+ * The size of an Eddystone-ETLM frame.
+ */
+ static const uint8_t FRAME_SIZE_ETLM = (FRAME_SIZE_TLM + 4);
+
+ // Nonce
+ static const uint8_t ETLM_NONCE_LEN = 6;
+ // Version
+ static const uint8_t VERSION_OFFSET = 4;
+ static const uint8_t TLM_VERSION = 0x00;
+ static const uint8_t ETLM_VERSION = 0x01;
+ // Data
+ static const uint8_t DATA_OFFSET = 5;
+ static const uint8_t TLM_DATA_LEN = 12;
+ static const uint8_t ETLM_DATA_LEN = 16;
+ // Salt
+ static const uint8_t SALT_OFFSET = 12;
+ static const uint8_t SALT_LEN = 2;
+ // Message Integrity Check
+ static const uint8_t MIC_OFFSET = 14;
+ static const uint8_t MIC_LEN = 2;
+ // Return codes
+ static const int ETLM_NONCE_INVALID_LEN = -1;
+
+ /**
+ * Constructs 6 byte (48-bit) Nonce from an empty array, rotationExp and beacon time (secs)
+ *
+ * @param[in] nonce
+ * the input and target nonce[] array
+ * @param[in] rotationPeriodExp
+ * Rotation exponent for EID
+ * @param[in] beaconTimeSecs
+ * Time in seconds since beacon boot.
+ * @return[out] return code (success = 0)
+ */
+ int generateEtlmNonce(uint8_t* nonce, uint8_t rotatePeriodExp, uint32_t beaconTimeSecs);
+
+
+private:
+
+ /**
+ * The size (in bytes) of an Eddystone-EID frame.
+ * This is the some of the Eddystone UUID(2 bytes), FrameType, AdvTxPower,
+ * EID Value
+ */
+ // static const uint8_t TLM_FRAME_LEN = 16;
+ // static const uint8_t ETLM_FRAME_LEN = 20;
+ static const uint8_t FRAME_LEN_OFFSET = 0;
+ static const uint8_t EDDYSTONE_UUID_LEN = 2;
+ static const uint8_t TLM_DATA_OFFSET = 3;
+ static const uint8_t ADV_FRAME_OFFSET = 1;
+
+ /**
+ * Eddystone-TLM version value.
+ */
+ uint8_t tlmVersion;
+ /**
+ * Time since boot in milliseconds.
+ */
+ uint32_t lastTimeSinceBootRead;
+ /**
+ * Eddystone-TLM Battery Voltage value.
+ */
+ uint16_t tlmBatteryVoltage;
+ /**
+ * Eddystone-TLM Beacon temperature value.
+ */
+ uint16_t tlmBeaconTemperature;
+ /**
+ * Eddystone-TLM Advertising PDU Count.
+ */
+ uint32_t tlmPduCount;
+ /**
+ * Eddystone-TLM time since boot with 0.1 second resolution.
+ */
+ uint32_t tlmTimeSinceBoot;
+
+
+};
+#endif /* __TLMFRAME_H__ */
diff --git a/implementations/mbed/source/UIDFrame.cpp b/implementations/mbed/source/UIDFrame.cpp
new file mode 100644
index 0000000..0120bac
--- /dev/null
+++ b/implementations/mbed/source/UIDFrame.cpp
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2006-2016 Google Inc, All Rights Reserved
+ *
+ * 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.
+ */
+
+#include "UIDFrame.h"
+
+UIDFrame::UIDFrame(void) {
+}
+
+void UIDFrame::clearFrame(uint8_t* frame) {
+ frame[FRAME_LEN_OFFSET] = 0; // Set frame length to zero to clear it
+}
+
+void UIDFrame::setData(uint8_t *rawFrame, int8_t advTxPower, const uint8_t* uidData) {
+ size_t index = 0;
+ rawFrame[index++] = UID_HEADER_LEN + UID_LENGTH; // UID length + overhead of four bytes below
+ rawFrame[index++] = EDDYSTONE_UUID[0]; // LSB 16-bit Eddystone UUID (little endian)
+ rawFrame[index++] = EDDYSTONE_UUID[1]; // MSB
+ rawFrame[index++] = FRAME_TYPE_UID; // 1B Type
+ rawFrame[index++] = advTxPower; // 1B Power @ 0meter
+
+ memcpy(rawFrame + index, uidData, UID_LENGTH); // UID = 10B NamespaceID + 6B InstanceID
+}
+
+uint8_t* UIDFrame::getData(uint8_t* rawFrame) {
+ return &(rawFrame[UID_DATA_OFFSET]);
+}
+
+uint8_t UIDFrame::getDataLength(uint8_t* rawFrame)
+{
+ return rawFrame[FRAME_LEN_OFFSET] - EDDYSTONE_UUID_LEN;
+}
+
+uint8_t* UIDFrame::getAdvFrame(uint8_t* rawFrame)
+{
+ return &(rawFrame[ADV_FRAME_OFFSET]);
+}
+
+uint8_t UIDFrame::getAdvFrameLength(uint8_t* rawFrame)
+{
+ return rawFrame[FRAME_LEN_OFFSET];
+}
+
+uint8_t* UIDFrame::getUid(uint8_t* rawFrame)
+{
+ return &(rawFrame[UID_VALUE_OFFSET]);
+}
+
+uint8_t UIDFrame::getUidLength(uint8_t* rawFrame)
+{
+ return rawFrame[FRAME_LEN_OFFSET] - UID_HEADER_LEN;
+}
+
+void UIDFrame::setAdvTxPower(uint8_t* rawFrame, int8_t advTxPower)
+{
+ rawFrame[UID_TXPOWER_OFFSET] = advTxPower;
+}
\ No newline at end of file
diff --git a/implementations/mbed/source/UIDFrame.h b/implementations/mbed/source/UIDFrame.h
new file mode 100644
index 0000000..6c02d4c
--- /dev/null
+++ b/implementations/mbed/source/UIDFrame.h
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2006-2016 Google Inc, All Rights Reserved
+ *
+ * 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.
+ */
+
+#ifndef __UIDFRAME_H__
+#define __UIDFRAME_H__
+
+#include
+#include "EddystoneTypes.h"
+
+/**
+ * Class that encapsulates data that belongs to the Eddystone-UID frame. For
+ * more information refer to https://github.com/google/eddystone/tree/master/eddystone-uid.
+ */
+class UIDFrame
+{
+public:
+ static const uint8_t UID_LENGTH = 16;
+
+ /**
+ * Construct a new instance of this class.
+ */
+ UIDFrame(void);
+
+ /**
+ * Clear frame (intervally indicated by length = 0 )
+ */
+ void clearFrame(uint8_t* frame);
+
+ /**
+ * Construct the raw bytes of the Eddystone-UID frame that will be directly
+ * used in the advertising packets.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ * @param[in] advPowerLevel
+ * Power level value included in the raw frame.
+ * @param[in] uidData
+ * The actual 16-byte UID data in the raw frame.
+ */
+ void setData(uint8_t* rawFrame, int8_t advTxPower, const uint8_t* uidData);
+
+ /**
+ * Get the UID frame data from the Eddystone-UID frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return A pointer to the bytes of the Eddystone-UID frame data.
+ */
+ uint8_t* getData(uint8_t* rawFrame);
+
+ /**
+ * Get the length of the UID frame data from the Eddystone-UID frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return The size in bytes of the Eddystone-UID frame.
+ */
+ uint8_t getDataLength(uint8_t* rawFrame);
+
+ /**
+ * Get the UID Adv data from the Eddystone-UID frame.
+ * This is the full service data included in the BLE service data params
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return A pointer to the bytes of the Eddystone-UID Adv frame data.
+ */
+ uint8_t* getAdvFrame(uint8_t* rawFrame);
+
+ /**
+ * Get the length of the UID Adv data from the Eddystone-UID frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return The size in bytes of the Eddystone-UID Adv frame data.
+ */
+ uint8_t getAdvFrameLength(uint8_t* rawFrame);
+
+ /**
+ * Get just the UID data from the Eddystone-UID frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return A pointer to the bytes of the UID in the Eddystone-UID frame.
+ */
+ uint8_t* getUid(uint8_t* rawFrame);
+
+ /**
+ * Get the length of just the UID data from the Eddystone-UID frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return The size in bytes of the UID in the Eddystone-UID frame.
+ */
+ uint8_t getUidLength(uint8_t* rawFrame);
+
+ /**
+ * Set the Adv TX Power in the frame. This is necessary because the adv
+ * Tx Power might be updated independent of the data bytes
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ * @param[in] advPowerLevel
+ * Power level value included in the raw frame.
+ *
+ */
+ void setAdvTxPower(uint8_t* rawFrame, int8_t advTxPower);
+
+ /**
+ * The byte ID of an Eddystone-UID frame.
+ */
+ static const uint8_t FRAME_TYPE_UID = 0x00;
+
+private:
+ static const uint8_t UID_FRAME_LEN = 20;
+ static const uint8_t FRAME_LEN_OFFSET = 0;
+ static const uint8_t EDDYSTONE_UUID_LEN = 2;
+ static const uint8_t UID_DATA_OFFSET = 3;
+ static const uint8_t ADV_FRAME_OFFSET = 1;
+ static const uint8_t UID_VALUE_OFFSET = 5;
+ static const uint8_t UID_HEADER_LEN = 4;
+ static const uint8_t UID_TXPOWER_OFFSET = 4;
+ /**
+ * The size (in bytes) of an Eddystone-UID frame.
+ * This is the some of the Eddystone UUID(2 bytes), FrameType, AdvTxPower,
+ * UID Name Length, and UID Instance Length
+ */
+ static const uint8_t FRAME_SIZE_UID = 20;
+ /**
+ * The size (in bytes) of an Eddystone-UID frame.
+ */
+ static const uint8_t UID_NAMESPACEID_LENGTH = 10;
+ static const uint8_t UID_INSTANCEID_LENGTH = 6;
+
+};
+
+#endif /* __UIDFRAME_H__ */
diff --git a/implementations/mbed/source/URLFrame.cpp b/implementations/mbed/source/URLFrame.cpp
new file mode 100644
index 0000000..02b4073
--- /dev/null
+++ b/implementations/mbed/source/URLFrame.cpp
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2006-2016 Google Inc, All Rights Reserved
+ *
+ * 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.
+ */
+
+#include "URLFrame.h"
+
+/* CONSTRUCTOR */
+URLFrame::URLFrame(void)
+{
+}
+
+void URLFrame::setUnencodedUrlData(uint8_t* rawFrame, int8_t advTxPower, const char *rawUrl)
+{
+ uint8_t encodedUrl[ENCODED_BUF_SIZE];
+ int encodedUrlLen = encodeURL(encodedUrl, rawUrl);
+ encodedUrlLen = (encodedUrlLen > MAX_URL_DATA) ? MAX_URL_DATA : encodedUrlLen;
+ setData(rawFrame, advTxPower, reinterpret_cast(encodedUrl), encodedUrlLen);
+}
+
+void URLFrame::clearFrame(uint8_t* frame) {
+ frame[FRAME_LEN_OFFSET] = 0; // Set frame length to zero to clear it
+}
+
+void URLFrame::setData(uint8_t* rawFrame, int8_t advTxPower, const uint8_t* encodedUrlData, uint8_t encodedUrlLen)
+{
+ uint8_t index = 0;
+ rawFrame[index++] = URL_HEADER_LEN + encodedUrlLen; // INDEX=0 = Frame Length = encodedURL size + 4 bytes of header below
+ rawFrame[index++] = EDDYSTONE_UUID[0]; // FRAME 16-bit Eddystone UUID Low Byte (little endian)
+ rawFrame[index++] = EDDYSTONE_UUID[1]; // FRAME 16-bit Eddystone UUID High Byte
+ rawFrame[index++] = FRAME_TYPE_URL; // URL Frame Type
+ rawFrame[index++] = advTxPower; // Power @ 0meter
+
+ memcpy(rawFrame + index, encodedUrlData, encodedUrlLen);
+}
+
+uint8_t* URLFrame::getData(uint8_t* rawFrame) {
+ return &(rawFrame[URL_DATA_OFFSET]);
+}
+
+
+uint8_t URLFrame::getDataLength(uint8_t* rawFrame) {
+ return rawFrame[FRAME_LEN_OFFSET] - EDDYSTONE_UUID_LEN;
+}
+
+uint8_t* URLFrame::getAdvFrame(uint8_t* rawFrame)
+{
+ return &(rawFrame[ADV_FRAME_OFFSET]);
+}
+
+uint8_t URLFrame::getAdvFrameLength(uint8_t* rawFrame)
+{
+ return rawFrame[FRAME_LEN_OFFSET];
+}
+
+uint8_t* URLFrame::getEncodedUrl(uint8_t* rawFrame)
+{
+ return &(rawFrame[URL_VALUE_OFFSET]);
+}
+
+uint8_t URLFrame::getEncodedUrlLength(uint8_t* rawFrame)
+{
+ return rawFrame[ADV_FRAME_OFFSET] - URL_HEADER_LEN;
+}
+
+
+uint8_t URLFrame::encodeURL(uint8_t* encodedUrl, const char *rawUrl)
+{
+ uint8_t urlDataLength = 0;
+
+ const char *prefixes[] = {
+ "http://www.",
+ "https://www.",
+ "http://",
+ "https://",
+ };
+ const size_t NUM_PREFIXES = sizeof(prefixes) / sizeof(char *);
+ const char *suffixes[] = {
+ ".com/",
+ ".org/",
+ ".edu/",
+ ".net/",
+ ".info/",
+ ".biz/",
+ ".gov/",
+ ".com",
+ ".org",
+ ".edu",
+ ".net",
+ ".info",
+ ".biz",
+ ".gov"
+ };
+ const size_t NUM_SUFFIXES = sizeof(suffixes) / sizeof(char *);
+
+ /*
+ * Fill with one more 0 than max url data size to ensure its null terminated
+ * And can be printed out for debug purposes
+ */
+ memset(encodedUrl, 0, MAX_URL_DATA + 1);
+
+ if ((rawUrl == NULL) || (strlen(rawUrl) == 0)) {
+ return urlDataLength;
+ }
+
+ /*
+ * handle prefix
+ */
+ for (size_t i = 0; i < NUM_PREFIXES; i++) {
+ size_t prefixLen = strlen(prefixes[i]);
+ if (strncmp(rawUrl, prefixes[i], prefixLen) == 0) {
+ encodedUrl[urlDataLength++] = i;
+ rawUrl += prefixLen;
+ break;
+ }
+ }
+
+ /*
+ * handle suffixes
+ */
+ while (*rawUrl && (urlDataLength <= MAX_URL_DATA)) {
+ /* check for suffix match */
+ size_t i;
+ for (i = 0; i < NUM_SUFFIXES; i++) {
+ size_t suffixLen = strlen(suffixes[i]);
+ if (strncmp(rawUrl, suffixes[i], suffixLen) == 0) {
+ encodedUrl[urlDataLength++] = i;
+ rawUrl += suffixLen;
+ break; /* from the for loop for checking against suffixes */
+ }
+ }
+ /* This is the default case where we've got an ordinary character which doesn't match a suffix. */
+ if (i == NUM_SUFFIXES) {
+ encodedUrl[urlDataLength++] = *rawUrl;
+ ++rawUrl;
+ }
+ }
+ return urlDataLength;
+}
+
+void URLFrame::setAdvTxPower(uint8_t* rawFrame, int8_t advTxPower)
+{
+ rawFrame[URL_TXPOWER_OFFSET] = advTxPower;
+}
diff --git a/implementations/mbed/source/URLFrame.h b/implementations/mbed/source/URLFrame.h
new file mode 100644
index 0000000..36a0202
--- /dev/null
+++ b/implementations/mbed/source/URLFrame.h
@@ -0,0 +1,185 @@
+/*
+ * Copyright (c) 2006-2016 Google Inc, All Rights Reserved
+ *
+ * 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.
+ */
+
+#ifndef __URLFRAME_H__
+#define __URLFRAME_H__
+
+#include "EddystoneTypes.h"
+#include
+
+/**
+ * Class that encapsulates data that belongs to the Eddystone-URL frame. For
+ * more information refer to https://github.com/google/eddystone/tree/master/eddystone-url.
+ */
+class URLFrame
+{
+public:
+ /**
+ * Construct a new instance of this class.
+ */
+ URLFrame(void);
+
+ /**
+ * Construct the raw bytes of the Eddystone-URL frame from an unencoded URL
+ * (a null terminated string) that will be directly used in the advertising
+ * packets.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ * @param[in] advPowerLevel
+ * Power level value included in the raw frame.
+ * @param[in] rawURL
+ * A null terminated string containing the URL
+ */
+ void setUnencodedUrlData(uint8_t* rawFrame, int8_t advTxPower, const char *rawUrl);
+
+ /**
+ * Clear frame (intervally indicated by length = 0 )
+ */
+ void clearFrame(uint8_t* frame);
+
+ /**
+ * Construct the raw bytes of the Eddystone-URL frame from an encoded URL
+ * plus length information
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ * @param[in] advPowerLevel
+ * Power level value included in the raw frame.
+ * @param[in] encodedUrlData
+ * A pointer to the encoded URL bytes.
+ * @param[in] encodedUrlLen
+ * The length in bytes of the encoded URL
+ */
+ void setData(uint8_t* rawFrame, int8_t advPowerLevel, const uint8_t* encodedUrlData, uint8_t encodedUrlLen);
+
+ /**
+ * Get the URL frame data from the Eddystone-URL frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return A pointer to the bytes of the Eddystone-URL frame data.
+ */
+ uint8_t* getData(uint8_t* rawFrame);
+
+ /**
+ * Get the length of the URL frame data from the Eddystone-UID frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return The size in bytes of the Eddystone-URL frame.
+ */
+ uint8_t getDataLength(uint8_t* rawFrame);
+
+ /**
+ * Get the URL Adv data from the Eddystone-URLframe.
+ * This is the full service data included in the BLE service data params
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return A pointer to the bytes of the Eddystone-URLAdv frame data.
+ */
+ uint8_t* getAdvFrame(uint8_t* rawFrame);
+
+ /**
+ * Get the length of the URLAdv data from the Eddystone-URL frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return The size in bytes of the Eddystone-URL Adv frame data.
+ */
+ uint8_t getAdvFrameLength(uint8_t* rawFrame);
+
+ /**
+ * Get just the encoded URL data from the Eddystone-URL frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return A pointer to the bytes of the encoded URL in the Eddystone-URL
+ * frame.
+ */
+ uint8_t* getEncodedUrl(uint8_t* rawFrame);
+
+ /**
+ * Get the length of just the encoded URL data from the Eddystone-URL frame.
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ *
+ * @return The size in bytes of the encoded URL in the Eddystone-URL frame.
+ */
+ uint8_t getEncodedUrlLength(uint8_t* rawFrame);
+
+ /**
+ * Set the Adv TX Power in the frame. This is necessary because the adv
+ * Tx Power might be updated independent of the data bytes
+ *
+ * @param[in] rawFrame
+ * Pointer to the location where the raw frame will be stored.
+ * @param[in] advPowerLevel
+ * Power level value included in the raw frame.
+ *
+ */
+ void setAdvTxPower(uint8_t* rawFrame, int8_t advTxPower);
+
+ /**
+ * Helper function that encodes a URL null terminated string into the HTTP
+ * URL Encoding required in Eddystone-URL frames. Refer to
+ * https://github.com/google/eddystone/blob/master/eddystone-url/README.md#eddystone-url-http-url-encoding.
+ *
+ * @param[in] encodedUrlData
+ * The encoded bytes of the URL
+ * @param[in] rawUrl
+ * The null terminated string containing a URL to encode.
+ * @return Length of the encodedData in bytes
+ */
+ static uint8_t encodeURL(uint8_t* encodedUrlData, const char* rawUrl);
+
+ /**
+ * The max size (in bytes) of an Eddystone-URL frame.
+ */
+ static const uint8_t ENCODED_BUF_SIZE = 32;
+ /**
+ * The byte ID of an Eddystone-URL frame.
+ */
+ static const uint8_t FRAME_TYPE_URL = 0x10;
+
+private:
+ static const uint8_t FRAME_LEN_OFFSET = 0;
+ static const uint8_t EDDYSTONE_UUID_LEN = 2;
+ static const uint8_t URL_DATA_OFFSET = 3;
+ static const uint8_t ADV_FRAME_OFFSET = 1;
+ static const uint8_t URL_VALUE_OFFSET = 5;
+ static const uint8_t URL_HEADER_LEN = 4;
+ static const uint8_t URL_TXPOWER_OFFSET = 4;
+
+ /**
+ * The minimum size (in bytes) of an Eddystone-URL frame.
+ */
+ static const uint8_t FRAME_MIN_SIZE_URL = 2;
+
+ /**
+ * Offset for playload in a rawFrame UID
+ */
+ static const uint8_t MAX_URL_DATA = 18;
+};
+
+#endif /* __URLFRAME_H__ */
diff --git a/implementations/mbed/source/aes_eax.cpp b/implementations/mbed/source/aes_eax.cpp
new file mode 100644
index 0000000..240d9f7
--- /dev/null
+++ b/implementations/mbed/source/aes_eax.cpp
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2016, Google Inc, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+
+#include
+
+// #include "aes_eax.h"
+// set defines before loading aes.h
+#define MBEDTLS_CIPHER_MODE_CBC
+#define MBEDTLS_CIPHER_MODE_CTR
+#include "mbedtls/aes.h"
+
+#define EDDY_ERR_EAX_AUTH_FAILED -0x000F /**< Authenticated decryption failed. */
+
+void gf128_double_( unsigned char val[16] )
+{
+ int i;
+ int carry = val[0] >> 7;
+ int xv = (-carry) & 0x87;
+ for (i = 15; i >= 0; i--) {
+ carry = val[i] >> 7;
+ val[i] = (val[i] << 1) ^ xv;
+ xv = carry;
+ }
+}
+
+int compute_cmac_( mbedtls_aes_context *ctx,
+ const unsigned char *input,
+ size_t length,
+ unsigned char param,
+ unsigned char mac[16] )
+{
+ unsigned char buf[16], iv[16];
+ memset(buf, 0, sizeof(buf));
+ buf[15] = param;
+ memset(iv, 0, sizeof(iv));
+ length += 16;
+
+ unsigned char pad[16];
+ memset(pad, 0, sizeof(pad));
+ mbedtls_aes_crypt_ecb(ctx, MBEDTLS_AES_ENCRYPT, pad, pad);
+ gf128_double_(pad);
+ if (length & 15) {
+ gf128_double_(pad);
+ pad[length & 15] ^= 0x80;
+ }
+
+ const unsigned char *tmp_input = buf;
+ while (length > 16) {
+ mbedtls_aes_crypt_cbc(ctx, MBEDTLS_AES_ENCRYPT, 16, iv, tmp_input, buf);
+ if (tmp_input == buf) {
+ tmp_input = input;
+ } else {
+ tmp_input += 16;
+ }
+ length -= 16;
+ }
+
+ size_t i;
+ for (i = 0; i < length; i++)
+ pad[i] ^= tmp_input[i];
+
+ mbedtls_aes_crypt_cbc(ctx, MBEDTLS_AES_ENCRYPT, 16, iv, pad, mac);
+ return 0;
+}
+
+int eddy_aes_authcrypt_eax( mbedtls_aes_context *ctx,
+ int mode,
+ const unsigned char *nonce,
+ size_t nonce_length,
+ const unsigned char *header,
+ size_t header_length,
+ size_t message_length,
+ const unsigned char *input,
+ unsigned char *output,
+ unsigned char *tag,
+ size_t tag_length )
+{
+ unsigned char header_mac[16];
+ unsigned char nonce_mac[16];
+ unsigned char ciphertext_mac[16];
+ uint8_t i;
+ compute_cmac_(ctx, header, header_length, 1, header_mac);
+ compute_cmac_(ctx, nonce, nonce_length, 0, nonce_mac);
+ if (mode == MBEDTLS_AES_DECRYPT) {
+ compute_cmac_(ctx, input, message_length, 2, ciphertext_mac);
+ unsigned char n_ok = 0;
+ for (i = 0; i < tag_length; i++) {
+ ciphertext_mac[i] ^= header_mac[i];
+ ciphertext_mac[i] ^= nonce_mac[i];
+ ciphertext_mac[i] ^= tag[i];
+ n_ok |= ciphertext_mac[i];
+ }
+ if (n_ok)
+ return EDDY_ERR_EAX_AUTH_FAILED;
+ }
+ size_t nc_off = 0;
+ unsigned char nonce_copy[16];
+ memcpy(nonce_copy, nonce_mac, sizeof(nonce_mac));
+ unsigned char sb[16];
+ mbedtls_aes_crypt_ctr(ctx, message_length, &nc_off, nonce_copy, sb, input, output);
+ if (mode == MBEDTLS_AES_ENCRYPT) {
+ compute_cmac_(ctx, output, message_length, 2, ciphertext_mac);
+ for (i = 0; i < tag_length; i++)
+ tag[i] = header_mac[i] ^ nonce_mac[i] ^ ciphertext_mac[i];
+ }
+ return 0;
+}
diff --git a/implementations/mbed/source/aes_eax.h b/implementations/mbed/source/aes_eax.h
new file mode 100644
index 0000000..93920a7
--- /dev/null
+++ b/implementations/mbed/source/aes_eax.h
@@ -0,0 +1,47 @@
+#if !defined(AES_EAX_H__INCLUDED__)
+#define AES_EAX_H__INCLUDED__
+
+#define MBEDTLS_CIPHER_MODE_CBC
+/*
+ * Copyright (c) 2016, Google Inc, All Rights Reserved
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * 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.
+ */
+
+#define MBEDTLS_CIPHER_MODE_CTR
+#include "mbedtls/aes.h"
+
+int compute_cmac_( mbedtls_aes_context *ctx,
+ const unsigned char *input,
+ size_t length,
+ unsigned char param,
+ unsigned char mac[16] );
+
+void gf128_double_( unsigned char val[16] );
+
+int eddy_aes_authcrypt_eax( mbedtls_aes_context *ctx,
+ int mode, /* ENCRYPT/DECRYPT */
+ const unsigned char *nonce, /* 48-bit nonce */
+ size_t nonce_length, /* = 6 */
+ const unsigned char *header, /* Empty buffer */
+ size_t header_length, /* = 0 */
+ size_t message_length, /* Length of input & output buffers 12 */
+ const unsigned char *input,
+ unsigned char *output,
+ unsigned char *tag,
+ size_t tag_length ); /* = 2 */
+
+
+
+#endif /* defined(AES_EAX_H__INCLUDED__) */
\ No newline at end of file
diff --git a/implementations/mbed/source/main.cpp b/implementations/mbed/source/main.cpp
new file mode 100644
index 0000000..449c3f0
--- /dev/null
+++ b/implementations/mbed/source/main.cpp
@@ -0,0 +1,289 @@
+/*
+ * Copyright (c) 2006-2016 Google Inc, All Rights Reserved
+ *
+ * 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.
+ */
+
+#ifdef YOTTA_CFG_MBED_OS // use minar on mbed OS
+# include "mbed-drivers/mbed.h"
+#else
+# include "mbed.h"
+#endif
+
+#include "ble/BLE.h"
+#include "EddystoneService.h"
+
+#include "PersistentStorageHelper/ConfigParamsPersistence.h"
+#include "stdio.h"
+
+#if (defined(NRF51) || defined(NRF52))
+ #include "nrf_soc.h"
+#endif
+
+// Instantiation of the main event loop for this program
+
+#ifdef YOTTA_CFG_MBED_OS // use minar on mbed OS
+# include "EventQueue/EventQueueMinar.h"
+ typedef eq::EventQueueMinar event_queue_t;
+
+#else // otherwise use the event classic queue
+# include "EventQueue/EventQueueClassic.h"
+ typedef eq::EventQueueClassic<
+ /* event count */ 10
+ > event_queue_t;
+
+#endif
+
+static event_queue_t eventQueue;
+
+EddystoneService *eddyServicePtr;
+
+/* Duration after power-on that config service is available. */
+static const int CONFIG_ADVERTISEMENT_TIMEOUT_SECONDS = EDDYSTONE_DEFAULT_CONFIG_ADVERTISEMENT_TIMEOUT_SECONDS;
+
+/* Values for ADV packets related to firmware levels, calibrated based on measured values at 1m */
+static const PowerLevels_t advTxPowerLevels = EDDYSTONE_DEFAULT_ADV_TX_POWER_LEVELS;
+/* Values for radio power levels, provided by manufacturer. */
+static const PowerLevels_t radioTxPowerLevels = EDDYSTONE_DEFAULT_RADIO_TX_POWER_LEVELS;
+
+DigitalOut configLED(CONFIG_LED, LED_OFF);
+
+static const int BLINKY_MSEC = 500; // How long to cycle config LED on/off
+static event_queue_t::event_handle_t handle = 0; // For the config mode timeout
+static event_queue_t::event_handle_t BlinkyHandle = 0; // For the blinking LED when in config mode
+
+static void blinky(void) { configLED = !configLED; }
+
+static void configLED_on(void) {
+ configLED = !LED_OFF;
+ BlinkyHandle = eventQueue.post_every(blinky, BLINKY_MSEC);
+}
+
+static void configLED_off(void) {
+ configLED = LED_OFF;
+ if (BlinkyHandle) {
+ eventQueue.cancel(BlinkyHandle);
+ BlinkyHandle = NULL;
+ }
+}
+
+/**
+ * Callback triggered some time after application started to switch to beacon mode.
+ */
+static void timeoutToStartEddystoneBeaconAdvertisements(void)
+{
+ Gap::GapState_t state;
+ state = BLE::Instance().gap().getState();
+ if (!state.connected) { /* don't switch if we're in a connected state. */
+ eddyServicePtr->startEddystoneBeaconAdvertisements();
+ configLED_off();
+ }
+}
+
+/**
+ * Callback triggered for a connection event.
+ */
+static void connectionCallback(const Gap::ConnectionCallbackParams_t *cbParams)
+{
+ (void) cbParams;
+ // Stop advertising whatever the current mode
+ eddyServicePtr->stopEddystoneBeaconAdvertisements();
+}
+
+/**
+ * Callback triggered for a disconnection event.
+ */
+static void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *cbParams)
+{
+ (void) cbParams;
+ BLE::Instance().gap().startAdvertising();
+ // Save params in persistent storage
+ EddystoneService::EddystoneParams_t params;
+ eddyServicePtr->getEddystoneParams(params);
+ saveEddystoneServiceConfigParams(¶ms);
+ // Ensure LED is off at the end of Config Mode or during a connection
+ configLED_off();
+ // 0.5 Second callback to rapidly re-establish Beaconing Service
+ // (because it needs to be executed outside of disconnect callback)
+ eventQueue.post_in(timeoutToStartEddystoneBeaconAdvertisements, 500 /* ms */);
+}
+
+// This section defines a simple push button handler to enter config or shutdown the beacon
+// Only compiles if "reset_button" is set in config.json in the "platform" section
+//
+#ifdef RESET_BUTTON
+
+InterruptIn button(RESET_BUTTON);
+DigitalOut shutdownLED(SHUTDOWN_LED, LED_OFF);
+
+static void shutdownLED_on(void) { shutdownLED = !LED_OFF; }
+static void shutdownLED_off(void) { shutdownLED = LED_OFF; }
+
+static bool beaconIsOn = true; // Button handler boolean to switch on or off
+static bool buttonBusy; // semaphore to make prevent switch bounce problems
+
+static void freeButtonBusy(void) { buttonBusy = false; }
+
+// Callback used to handle button presses from thread mode (not IRQ)
+static void button_task(void) {
+ bool locked = eddyServicePtr->isLocked();
+
+ // only shutdown if ON and unlocked
+ if (beaconIsOn && !locked) {
+ eventQueue.cancel(handle); // kill any pending callback tasks
+ beaconIsOn = false;
+ eddyServicePtr->stopEddystoneBeaconAdvertisements();
+ configLED_off(); // just in case it's still running...
+ shutdownLED_on(); // Flash shutdownLED to let user know we're turning off
+ eventQueue.post_in(shutdownLED_off, 1000);
+ // only go into configMode if OFF or locked and not in configMode
+ } else if (!beaconIsOn || (locked && BlinkyHandle == NULL)) {
+ eventQueue.cancel(handle); // kill any pending callback tasks
+ beaconIsOn = true;
+ eddyServicePtr->startEddystoneConfigAdvertisements();
+ configLED_on();
+ handle = eventQueue.post_in(
+ timeoutToStartEddystoneBeaconAdvertisements,
+ CONFIG_ADVERTISEMENT_TIMEOUT_SECONDS * 1000 /* ms */
+ );
+ }
+ eventQueue.post_in(freeButtonBusy, 750 /* ms */);
+}
+
+/**
+ * Raw IRQ handler for the reset button. We don't want to actually do any work here.
+ * Instead, we queue work to happen later using an event queue, by posting a callback.
+ * This has the added avantage of serialising actions, so if the button press happens
+ * during the config->beacon mode transition timeout, the button_task won't happen
+ * until the previous task has finished.
+ *
+ * If your buttons aren't debounced, you should do this in software, or button_task
+ * might get queued multiple times.
+ */
+static void reset_rise(void)
+{
+ if (!buttonBusy) {
+ buttonBusy = true;
+ eventQueue.post(button_task);
+ }
+}
+#endif
+
+static void onBleInitError(BLE::InitializationCompleteCallbackContext* initContext)
+{
+ /* Initialization error handling goes here... */
+ (void) initContext;
+}
+
+
+static void bleInitComplete(BLE::InitializationCompleteCallbackContext* initContext)
+{
+ BLE &ble = initContext->ble;
+ ble_error_t error = initContext->error;
+
+ if (error != BLE_ERROR_NONE) {
+ onBleInitError(initContext);
+ return;
+ }
+
+ ble.gap().onDisconnection(disconnectionCallback);
+
+ ble.gap().onConnection(connectionCallback);
+
+ EddystoneService::EddystoneParams_t params;
+
+ wait_ms(35); // Allow the RNG number generator to collect data
+
+ // Determine if booting directly after re-Flash or not
+ if (loadEddystoneServiceConfigParams(¶ms)) {
+ // 2+ Boot after reflash, so get parms from Persistent Storage
+ eddyServicePtr = new EddystoneService(ble, params, radioTxPowerLevels, eventQueue);
+ } else {
+ // 1st Boot after reflash, so reset everything to defaults
+ /* NOTE: slots are initialized in the constructor from the config.json file */
+ eddyServicePtr = new EddystoneService(ble, advTxPowerLevels, radioTxPowerLevels, eventQueue);
+ }
+
+ // Save Default params in persistent storage ready for next boot event
+ eddyServicePtr->getEddystoneParams(params);
+ saveEddystoneServiceConfigParams(¶ms);
+ // Start the Eddystone Config service - This will never stop (only connectability will change)
+ eddyServicePtr->startEddystoneConfigService();
+
+ /* Start Eddystone config Advertizements (to initialize everything properly) */
+ configLED_on();
+ eddyServicePtr->startEddystoneConfigAdvertisements();
+ handle = eventQueue.post_in(
+ timeoutToStartEddystoneBeaconAdvertisements,
+ CONFIG_ADVERTISEMENT_TIMEOUT_SECONDS * 1000 /* ms */
+ );
+
+#if (defined(NRF51) || defined(NRF52))
+ sd_power_dcdc_mode_set(NRF_POWER_DCDC_ENABLE); // set the DCDC mode for the Nordic chip to lower power consumption
+#endif
+
+ // now shut everything off (used for final beacon that ships w/ battery)
+#ifdef RESET_BUTTON
+ eventQueue.post_in(button_task, 2000 /* ms */);
+#endif
+}
+
+void app_start(int, char *[])
+{
+
+#ifdef NO_LOGGING
+ /* Tell standard C library to not allocate large buffers for these streams */
+ // setbuf(stdout, NULL);
+ // setbuf(stderr, NULL);
+ // setbuf(stdin, NULL);
+#endif
+
+#ifndef NO_4SEC_START_DELAY
+ // delay ~4secs before starting to allow time the nRF51 hardware to settle
+ // Also allows time to attach a virtual terimal to read logging output during init
+ wait_ms(4000);
+#endif
+
+#ifdef RESET_BUTTON
+ beaconIsOn = true; // Booting up, initialize for button handler
+ buttonBusy = false; // software debouncing of the reset button
+ button.rise(&reset_rise); // setup reset button
+#endif
+
+ BLE &ble = BLE::Instance();
+ ble.init(bleInitComplete);
+}
+
+#if !defined(YOTTA_CFG_MBED_OS)
+
+void scheduleBleEventsProcessing(BLE::OnEventsToProcessCallbackContext* context) {
+ eventQueue.post(&BLE::processEvents, &context->ble);
+ }
+
+int main() {
+
+ BLE &ble = BLE::Instance();
+ ble.onEventsToProcess(scheduleBleEventsProcessing);
+
+ app_start(0, NULL);
+
+ while (true) {
+ eventQueue.dispatch();
+ sleep();
+ }
+
+ return 0;
+}
+
+
+#endif
diff --git a/implementations/mbed/source/mbedtls_config.h b/implementations/mbed/source/mbedtls_config.h
new file mode 100644
index 0000000..e463729
--- /dev/null
+++ b/implementations/mbed/source/mbedtls_config.h
@@ -0,0 +1,112 @@
+#ifndef MBEDTLS_EDDYSTONE_CONFIG_H
+#define MBEDTLS_EDDYSTONE_CONFIG_H
+
+/* System support */
+//#define MBEDTLS_HAVE_ASM
+#undef MBEDTLS_HAVE_ASM
+#define MBEDTLS_HAVE_TIME
+#undef MBEDTLS_HAVE_TIME_DATE
+
+#define MBEDTLS_CIPHER_MODE_CBC
+#define MBEDTLS_CIPHER_MODE_CTR
+#undef MBEDTLS_CIPHER_PADDING_PKCS7
+#undef MBEDTLS_REMOVE_ARC4_CIPHERSUITES
+
+/* mbed TLS feature support */
+#define MBEDTLS_ECP_DP_SECP256R1_ENABLED
+#undef MBEDTLS_ECP_DP_SECP384R1_ENABLED
+#define MBEDTLS_ECP_DP_CURVE25519_ENABLED
+
+#undef MBEDTLS_ECP_NIST_OPTIM
+#undef MBEDTLS_ECDSA_DETERMINISTIC
+#undef MBEDTLS_KEY_EXCHANGE_PSK_ENABLED
+#undef MBEDTLS_KEY_EXCHANGE_ECDHE_PSK_ENABLED
+#undef MBEDTLS_KEY_EXCHANGE_ECDHE_RSA_ENABLED
+#undef MBEDTLS_KEY_EXCHANGE_ECDHE_ECDSA_ENABLED
+#undef MBEDTLS_ERROR_STRERROR_DUMMY
+#undef MBEDTLS_PK_RSA_ALT_SUPPORT
+#undef MBEDTLS_PKCS1_V15
+#undef MBEDTLS_PKCS1_V21
+#undef MBEDTLS_SELF_TEST
+#undef MBEDTLS_SSL_ALL_ALERT_MESSAGES
+#undef MBEDTLS_SSL_ENCRYPT_THEN_MAC
+#undef MBEDTLS_SSL_EXTENDED_MASTER_SECRET
+#undef MBEDTLS_SSL_RENEGOTIATION
+#undef MBEDTLS_SSL_MAX_FRAGMENT_LENGTH
+#undef MBEDTLS_SSL_PROTO_TLS1_2
+#undef MBEDTLS_SSL_PROTO_DTLS
+#undef MBEDTLS_SSL_ALPN
+#undef MBEDTLS_SSL_DTLS_ANTI_REPLAY
+#undef MBEDTLS_SSL_DTLS_HELLO_VERIFY
+#undef MBEDTLS_SSL_DTLS_CLIENT_PORT_REUSE
+#undef MBEDTLS_SSL_DTLS_BADMAC_LIMIT
+#undef MBEDTLS_SSL_SESSION_TICKETS
+#undef MBEDTLS_SSL_EXPORT_KEYS
+#undef MBEDTLS_SSL_SESSION_TICKETS
+#undef MBEDTLS_SSL_EXPORT_KEYS
+#undef MBEDTLS_SSL_SERVER_NAME_INDICATION
+#undef MBEDTLS_VERSION_FEATURES
+#undef MBEDTLS_X509_CHECK_KEY_USAGE
+#undef MBEDTLS_X509_CHECK_EXTENDED_KEY_USAGE
+
+/* mbed TLS modules */
+#define MBEDTLS_AES_C
+#undef MBEDTLS_ASN1_PARSE_C
+#undef MBEDTLS_ASN1_WRITE_C
+#undef MBEDTLS_BASE64_C
+#define MBEDTLS_BIGNUM_C
+#undef MBEDTLS_CCM_C
+#undef MBEDTLS_CERTS_C
+#undef MBEDTLS_CIPHER_C
+#define MBEDTLS_CTR_DRBG_C
+#undef MBEDTLS_DEBUG_C
+#define MBEDTLS_ECDH_C
+#undef MBEDTLS_ECDSA_C
+#define MBEDTLS_ECP_C
+#define MBEDTLS_ENTROPY_C
+#undef MBEDTLS_ERROR_C
+#undef MBEDTLS_GCM_C
+#undef MBEDTLS_HMAC_DRBG_C
+#define MBEDTLS_MD_C
+#undef MBEDTLS_OID_C
+#undef MBEDTLS_PEM_PARSE_C
+#undef MBEDTLS_PK_C
+#undef MBEDTLS_PK_PARSE_C
+#undef MBEDTLS_PK_WRITE_C
+#undef MBEDTLS_PLATFORM_C
+#undef MBEDTLS_RSA_C
+#define MBEDTLS_SHA256_C
+#undef MBEDTLS_SHA512_C
+#undef MBEDTLS_SSL_CACHE_C
+#undef MBEDTLS_SSL_COOKIE_C
+#undef MBEDTLS_SSL_TICKET_C
+#undef MBEDTLS_SSL_CLI_C
+#undef MBEDTLS_SSL_SRV_C
+#undef MBEDTLS_SSL_TLS_C
+#undef MBEDTLS_VERSION_C
+#undef MBEDTLS_X509_USE_C
+#undef MBEDTLS_X509_CRT_PARSE_C
+#undef MBEDTLS_X509_CRL_PARSE_C
+
+/* Save RAM at the expense of ROM */
+#define MBEDTLS_AES_ROM_TABLES
+
+/* Save RAM by adjusting to our exact needs */
+#define MBEDTLS_ECP_MAX_BITS 256
+#define MBEDTLS_MPI_MAX_SIZE 32
+
+/* Save RAM at the expense of speed, see ecp.h */
+#define MBEDTLS_ECP_WINDOW_SIZE 2
+#define MBEDTLS_ECP_FIXED_POINT_OPTIM 0
+
+/* Significant speed benefit at the expense of some ROM */
+#define MBEDTLS_ECP_NIST_OPTIM
+/*
+* You should adjust this to the exact number of sources you're using: default
+* is the "mbedtls_platform_entropy_poll" source, but you may want to add other ones.
+* Minimum is 2 for the entropy test suite.
+*/
+#define MBEDTLS_ENTROPY_MAX_SOURCES 2
+#define MBEDTLS_NO_PLATFORM_ENTROPY
+
+#endif /* MBEDTLS_EDDYSTONE_CONFIG_H */
diff --git a/tools/gatt-config/README.md b/tools/gatt-config/README.md
new file mode 100644
index 0000000..93f5bc6
--- /dev/null
+++ b/tools/gatt-config/README.md
@@ -0,0 +1,3 @@
+Android and iOS apps developed at Google by our amazing interns, Galia and Denisa.
+
+TODO: link up once pushed
diff --git a/tools/gatt-config/android/BeaconConfig/.gitignore b/tools/gatt-config/android/BeaconConfig/.gitignore
new file mode 100644
index 0000000..c6cbe56
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/.gitignore
@@ -0,0 +1,8 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
diff --git a/tools/gatt-config/android/BeaconConfig/.idea/.name b/tools/gatt-config/android/BeaconConfig/.idea/.name
new file mode 100644
index 0000000..013b0b6
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/.idea/.name
@@ -0,0 +1 @@
+BeaconConfig
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/.idea/compiler.xml b/tools/gatt-config/android/BeaconConfig/.idea/compiler.xml
new file mode 100644
index 0000000..96cc43e
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/.idea/compiler.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/.idea/copyright/profiles_settings.xml b/tools/gatt-config/android/BeaconConfig/.idea/copyright/profiles_settings.xml
new file mode 100644
index 0000000..e7bedf3
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/.idea/copyright/profiles_settings.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/.idea/encodings.xml b/tools/gatt-config/android/BeaconConfig/.idea/encodings.xml
new file mode 100644
index 0000000..97626ba
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/.idea/encodings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/.idea/gradle.xml b/tools/gatt-config/android/BeaconConfig/.idea/gradle.xml
new file mode 100644
index 0000000..508b3d9
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/.idea/gradle.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/.idea/misc.xml b/tools/gatt-config/android/BeaconConfig/.idea/misc.xml
new file mode 100644
index 0000000..1a3eaff
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/.idea/misc.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/.idea/modules.xml b/tools/gatt-config/android/BeaconConfig/.idea/modules.xml
new file mode 100644
index 0000000..cb7cbc7
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/.idea/modules.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/.idea/runConfigurations.xml b/tools/gatt-config/android/BeaconConfig/.idea/runConfigurations.xml
new file mode 100644
index 0000000..7f68460
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/.idea/runConfigurations.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/.gitignore b/tools/gatt-config/android/BeaconConfig/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/tools/gatt-config/android/BeaconConfig/app/app-release.apk b/tools/gatt-config/android/BeaconConfig/app/app-release.apk
new file mode 100644
index 0000000..56d1615
Binary files /dev/null and b/tools/gatt-config/android/BeaconConfig/app/app-release.apk differ
diff --git a/tools/gatt-config/android/BeaconConfig/app/build.gradle b/tools/gatt-config/android/BeaconConfig/app/build.gradle
new file mode 100644
index 0000000..5bf5bb5
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/build.gradle
@@ -0,0 +1,39 @@
+apply plugin: 'com.android.application'
+
+android {
+ compileSdkVersion 23
+ buildToolsVersion "23.0.3"
+
+ defaultConfig {
+ applicationId "com.github.google.beaconfig"
+ minSdkVersion 21
+ targetSdkVersion 23
+ versionCode 1
+ versionName "1.0"
+ multiDexEnabled true
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+dependencies {
+ compile 'com.android.support:appcompat-v7:23.4.0'
+ compile 'com.android.support:cardview-v7:23.4.0'
+ compile 'com.android.support:design:23.4.0'
+ compile 'com.android.support:recyclerview-v7:23.4.0'
+ compile 'com.android.support:support-v4:23.4.0'
+ compile 'com.android.support:multidex:1.0.0'
+ compile 'com.google.code.gson:gson:2.2.4'
+ androidTestCompile 'com.android.support.test:runner:0.4'
+ // Set this dependency to use JUnit 4 rules
+ androidTestCompile 'com.android.support.test:rules:0.4'
+ // Set this dependency to build and run Espresso tests
+ androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
+ // Set this dependency to build and run UI Automator tests
+ androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
+ androidTestCompile 'com.android.support:support-annotations:23.4.0'
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/proguard-rules.pro b/tools/gatt-config/android/BeaconConfig/app/proguard-rules.pro
new file mode 100644
index 0000000..0204364
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /usr/local/google/home/galinap/Android/Sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/AndroidManifest.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..41ee5a3
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/AndroidManifest.xml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconConfigActivity.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconConfigActivity.java
new file mode 100644
index 0000000..fa69e46
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconConfigActivity.java
@@ -0,0 +1,965 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig;
+
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.design.widget.TabLayout;
+import android.support.v4.view.ViewPager;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import com.github.google.beaconfig.dialogs.ConfirmationDialog;
+import com.github.google.beaconfig.dialogs.SaveConfigurationDialog;
+import com.github.google.beaconfig.dialogs.UnlockDialog;
+import com.github.google.beaconfig.gatt.GattClient;
+import com.github.google.beaconfig.gatt.GattClientException;
+import com.github.google.beaconfig.gatt.GattConstants;
+import com.github.google.beaconfig.gatt.GattOperationException;
+import com.github.google.beaconfig.utils.BroadcastCapabilities;
+import com.github.google.beaconfig.utils.UiUtils;
+import com.github.google.beaconfig.utils.Utils;
+import layout.BeaconTabFragment;
+import layout.GlobalFragment;
+import layout.SlotFragment;
+
+/**
+ * Activity for connecting to the beacon, unlocking it, reading its characteristics
+ * and configuring them.
+ */
+public class BeaconConfigActivity extends AppCompatActivity
+ implements SlotFragment.ConfigurationListener {
+ private static final String TAG = BeaconConfigActivity.class.getSimpleName();
+
+ private BroadcastCapabilities capabilities;
+ private GattClient gattClient;
+ private String unlockCode;
+ private String address;
+ private String name;
+
+ private SlotsAdapter slotsAdapter;
+ private ViewPager viewPager;
+ private SwipeRefreshLayout swipeRefreshLayout;
+
+ private boolean intendedDisconnection = false;
+
+ private ExecutorService executor;
+ private SavedConfigurationsManager configurationsManager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_beacon_config);
+
+ name = getIntent().getStringExtra(BeaconListAdapter.ViewHolder.BEACON_NAME);
+ address = getIntent().getStringExtra(BeaconListAdapter.ViewHolder.BEACON_ADDRESS);
+
+ setupToolbar(name, address);
+ setUpThrobber();
+ executor = Executors.newSingleThreadExecutor();
+ configurationsManager = new SavedConfigurationsManager(this);
+
+ findViewById(R.id.grey_out_slot).setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ return swipeRefreshLayout.isRefreshing();
+ }
+ });
+
+ accessBeacon();
+ }
+
+ /**
+ * Starts the process of connecting to the beacon, discovering its services, unlocking it and
+ * reading all available information from it.
+ */
+ private void accessBeacon() {
+ disableDisplay();
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ connectToGatt(address);
+ }
+ });
+ //if connected, discovering devices takes place in onGattConnected() in the GattListener
+ }
+
+ private void connectToGatt(String address) {
+ Log.d(TAG, "Connecting...");
+ UiUtils.showToast(this, "Connecting...");
+ gattClient = new GattClient(getApplicationContext(), address, gattListener);
+ if (gattClient == null) {
+ return;
+ }
+ gattClient.connect();
+ }
+
+ private GattClient.GattListener gattListener = new GattClient.GattListener() {
+ @Override
+ public void onGattConnected() {
+ Log.d(TAG, "Connected to GATT service.");
+ gattClient.discoverServices();
+ }
+
+ @Override
+ public void onGattServicesDiscovered() {
+ Log.d(TAG, "Discovered GATT services.");
+ if (gattClient.isEddystoneGattServicePresent()) {
+ try {
+ byte[] lockState = gattClient.readLockState();
+ if (lockState[0] == GattConstants.LOCK_STATE_LOCKED) {
+ unlock();
+ } else {
+ setupBeaconInformationDisplay();
+ }
+
+ } catch (GattClientException e) {
+ Log.e(TAG, "Gatt Client Error when discovering devices", e);
+ displayConnectionFailScreen("Something went wrong when discovering services "
+ + "of beacon");
+ } catch (GattOperationException e) {
+ Log.e(TAG, "Gatt Operation Error when discovering devices", e);
+ displayConnectionFailScreen("Something went wrong when discovering services "
+ + "of beacon");
+ }
+ } else {
+ gattClient.disconnect();
+ }
+ }
+
+ @Override
+ public void onGattDisconnected() {
+ Log.d(TAG, "Beacon disconnected.");
+ if (intendedDisconnection) {
+ UiUtils.showToast(BeaconConfigActivity.this, "Beacon disconnected.");
+ intendedDisconnection = false;
+ } else if (!gattClient.isEddystoneGattServicePresent()){
+ displayConnectionFailScreen("This beacon does not support the GATT service "
+ + "and cannot be configured with this app. \n \n");
+ } else {
+ displayConnectionFailScreen("Connection to beacon was lost unexpectedly. \n \n"
+ + getResources().getString(R.string.connect_to_beacon_message));
+ }
+ }
+ };
+
+ /**
+ * Starts the process of unlocking a beacon. First it tries to automatically unlock the beacon
+ * by applying the most common lock codes like all "f"s or all "0"s. If this fails, it pops up
+ * a dialog to ask the user for the lock code of the beacon.
+ */
+ private void unlock() {
+ Runnable setupBeaconInformationDisplay = new Runnable() {
+ @Override
+ public void run() {
+ setupBeaconInformationDisplay();
+ }
+ };
+
+ attemptAutomaticUnlock(setupBeaconInformationDisplay);
+ }
+
+ private void attemptAutomaticUnlock(final Runnable runnable) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ ArrayList commonPasswords = new ArrayList<>();
+ commonPasswords.add("ffffffffffffffffffffffffffffffff");
+ commonPasswords.add("00000000000000000000000000000000");
+
+ for (String password : commonPasswords) {
+ if (gattClient.unlock(Utils.toByteArray(password))) {
+ Log.d(TAG, "Beacon unlocked automatically");
+ unlockCode = password;
+ runnable.run();
+ return;
+ }
+ }
+
+ //if automatic unlock fails, a dialog pops up to ask the user for the beacon's lock
+ // code
+ attemptManualUnlock(runnable);
+ }
+ });
+ }
+
+ private void attemptManualUnlock(final Runnable runnable) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ UnlockDialog.show(BeaconConfigActivity.this, new UnlockDialog.UnlockListener() {
+ @Override
+ public void unlockingDismissed() {
+ displayConnectionFailScreen("This beacon is locked. \n \n"
+ + "It has to be unlocked before accessing its characteristics.");
+ }
+
+ @Override
+ public void unlock(byte[] unlockCode) {
+ String unlockCodeString = Utils.toHexString(unlockCode);
+ Log.d(TAG, "Trying " + unlockCodeString);
+ if (gattClient.unlock(unlockCode)) {
+ BeaconConfigActivity.this.unlockCode = unlockCodeString;
+ if (runnable != null) {
+ runnable.run();
+ }
+ } else {
+ UiUtils.showToast(BeaconConfigActivity.this,
+ "Incorrect lock code. Try again?");
+ attemptManualUnlock(runnable);
+ }
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Displays a screen with a message and a "try again" button. Pressing the button will attempt
+ * to connect to the beacon from beginning
+ *
+ * @param message message which we want to be printed on the screen
+ */
+ private void displayConnectionFailScreen(final String message) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ enableDisplay();
+ ViewGroup configContentView
+ = (ViewGroup) findViewById(R.id.beacon_config_page_content);
+ UiUtils.makeChildrenInvisible(configContentView);
+ ViewGroup connectionFailSlot = (ViewGroup) findViewById(R.id.connection_fail);
+ connectionFailSlot.findViewById(R.id.connection_fail_btn)
+ .setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ accessBeacon();
+ findViewById(R.id.connection_fail).setVisibility(View.GONE);
+ }
+ });
+ ((TextView) connectionFailSlot.findViewById(R.id.connection_fail_message))
+ .setText(message);
+ connectionFailSlot.setVisibility(View.VISIBLE);
+ }
+ });
+
+ }
+
+ /**
+ * Called after successful connection to the beacon is made and services are
+ * discovered successfully.
+ *
+ * It sets up the beacon configuration screen with tabs which are controlled by a view pager and
+ * an adapter.
+ */
+ private void setupBeaconInformationDisplay() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ viewPager = (ViewPager) findViewById(R.id.view_pager);
+ slotsAdapter = new SlotsAdapter(getSupportFragmentManager());
+ viewPager.setAdapter(slotsAdapter);
+
+ TabLayout tabs = (TabLayout) findViewById(R.id.tabs);
+ tabs.setupWithViewPager(viewPager);
+ tabs.setTabGravity(TabLayout.GRAVITY_CENTER);
+ tabs.setTabMode(TabLayout.MODE_SCROLLABLE);
+ setUpFragments();
+ }
+ });
+ }
+
+ /**
+ * Reads information about the beacon from the gatt client. It creates a fragment
+ * for each slot of the beacon and puts it in the tab layout. Then all the available information
+ * about this slot is read and displayed on the screen
+ *
+ * All operations do not involve UI are done on the background thread
+ */
+ private void setUpFragments() {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ byte[] data = gattClient.readBroadcastCapabilities();
+ capabilities = new BroadcastCapabilities(data);
+
+ Bundle globalDataBundle = new Bundle();
+ globalDataBundle.putByteArray(Constants.BROADCAST_CAPABILITIES, data);
+ globalDataBundle.putString(Constants.BEACON_ADDRESS, address);
+ globalDataBundle.putString(Constants.BEACON_NAME, name);
+
+ byte[] remainConnectable = gattClient.readRemainConnectable();
+ globalDataBundle.putByte(Constants.REMAIN_CONNECTABLE, remainConnectable[0]);
+
+ if (!capabilities.isVariableTxPowerSupported()) {
+ String radioTxPower = Integer.toString(gattClient.readRadioTxPower()[0]);
+ String advTxPower = Integer.toString(gattClient.readAdvertisedTxPower()[0]);
+ globalDataBundle.putString(Constants.TX_POWER, radioTxPower);
+ globalDataBundle.putString(Constants.ADV_POWER, advTxPower);
+ }
+ if (!capabilities.isVariableAdvSupported()) {
+ String advInterval
+ = Integer.toString(Utils.toInt
+ (gattClient.readAdvertisingInterval()));
+ globalDataBundle.putString(Constants.ADV_INTERVAL, advInterval);
+ }
+
+ GlobalFragment globalFragment = GlobalFragment.newInstance(globalDataBundle);
+ slotsAdapter.addFragment(globalFragment);
+
+ for (int i = 0; i < capabilities.getMaxSupportedTotalSlots(); i++) {
+ gattClient.writeActiveSlot(i);
+ final Bundle slotInfoBundle = new Bundle();
+ final byte[] slotData = gattClient.readAdvSlotData();
+ slotInfoBundle.putByteArray(Constants.SLOT_DATA, slotData);
+
+ if (capabilities.isVariableTxPowerSupported()) {
+ String radioTxPower = Integer.toString
+ (gattClient.readRadioTxPower()[0]);
+ String advTxPower = Integer.toString
+ (gattClient.readAdvertisedTxPower()[0]);
+ slotInfoBundle.putString(Constants.TX_POWER, radioTxPower);
+ slotInfoBundle.putString(Constants.ADV_POWER, advTxPower);
+ }
+
+ if (capabilities.isVariableAdvSupported()) {
+ String advInterval = Integer.toString
+ (Utils.toInt(gattClient.readAdvertisingInterval()));
+ slotInfoBundle.putString(Constants.ADV_INTERVAL, advInterval);
+ }
+
+ slotInfoBundle.putByteArray(Constants.BROADCAST_CAPABILITIES, data);
+
+ slotInfoBundle.putInt(Constants.SLOT_NUMBER, i);
+
+ slotsAdapter.createNewFragment(slotInfoBundle);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ slotsAdapter.notifyDataSetChanged();
+ }
+ });
+ }
+
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ viewPager.setOffscreenPageLimit(
+ capabilities.getMaxSupportedTotalSlots());
+ setTabTitles();
+ enableDisplay();
+ findViewById(R.id.tab_layout).setVisibility(View.VISIBLE);
+ }
+ });
+ } catch (GattClientException e) {
+ Log.d(TAG, "Gatt Client Error when getting information from beacon", e);
+ displayConnectionFailScreen("Something went wrong when getting information "
+ + "from beacon");
+ } catch (GattOperationException e) {
+ Log.d(TAG, "Gatt Operation Error when getting information from beacon", e);
+ displayConnectionFailScreen("Something went wrong when getting information "
+ + "from beacon");
+ }
+
+ }
+ });
+ }
+
+ private void setTabTitles() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ ((TabLayout) findViewById(R.id.tabs)).getTabAt(0).setIcon(R.drawable.globe);
+ TabLayout tabs = (TabLayout) findViewById(R.id.tabs);
+ for (int i = 1; i < tabs.getTabCount(); i++) {
+ tabs.getTabAt(i).setText(slotsAdapter.getItem(i).name);
+ }
+ }
+ });
+ }
+
+ private void setUpThrobber() {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.throbber);
+ swipeRefreshLayout.setColorSchemeResources(android.R.color.holo_blue_bright,
+ android.R.color.holo_green_light,
+ android.R.color.holo_orange_light,
+ android.R.color.holo_red_light);
+ swipeRefreshLayout.setEnabled(false);
+ }
+ });
+ }
+
+ private void setupToolbar(final String name, final String address) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ toolbar.setSubtitle(address);
+ getWindow().setStatusBarColor(getResources().getColor(R.color.colorPrimary));
+ setTitle(name);
+ }
+ });
+ }
+
+ /**
+ * Attempts to save all the changes that the user has introduced to any of the beacon's slots
+ * by writing values to the gatt client
+ *
+ * Runs on the background thread
+ */
+ private void saveChangesAcrossAllTabs() {
+ disableDisplay();
+
+ Utils.hideKeyboard(this, findViewById(R.id.tab_layout));
+
+ final Runnable saveAllChangesTask = new Runnable() {
+ @Override
+ public void run() {
+ for (int i = 0; i < slotsAdapter.getCount(); i++) {
+ slotsAdapter.getItem(i).saveChanges();
+ }
+ }
+ };
+
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ if (areAllSlotsAreEmpty()) {
+ ConfirmationDialog.confirm("Save empty beacon", "You are about to configure "
+ + "this beacon to broadcast empty frame from all of its slots. This "
+ + "means that you will not be able to detect this beacon with this "
+ + "app anymore and will not be able to configure it. \n\n Are you "
+ + "sure you want to do this?", "YES", "NO", BeaconConfigActivity.this,
+ new ConfirmationDialog.ConfirmationListener() {
+ @Override
+ public void confirm() {
+ saveAllChangesTask.run();
+ }
+
+ @Override
+ public void cancel() {
+
+ }
+ });
+ } else {
+ saveAllChangesTask.run();
+ }
+
+
+ enableDisplay();
+ UiUtils.showToast(BeaconConfigActivity.this,
+ getResources().getString(R.string.changes_save_successful));
+ }
+ });
+ }
+
+ private boolean areAllSlotsAreEmpty() {
+ for (int i = 1; i < slotsAdapter.getCount(); i++) {
+ if (!((SlotFragment) slotsAdapter.getItem(i)).isEmpty()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Starts the process of saving the configuration of the connected beacon on a background
+ * thread. It pops a dialog asking the user to enter a name under which the current
+ * configuration will be saved. After this the user will be able to choose this name to apply
+ * this configuration to any beacon.
+ */
+ private void saveCurrentConfiguration() {
+ disableDisplay();
+
+ SaveConfigurationDialog.show(this, new SaveConfigurationDialog.SaveConfigListener() {
+ @Override
+ public void configNameChosen(String configName) {
+ saveCurrentConfigurationWithName(configName);
+ }
+ });
+ }
+
+ /**
+ * Saves the current configuration of the connected beacon. This includes information about all
+ * slots - slotData, tx power, adv tx power, adv interval.
+ *
+ * All of this is done on a background thread
+ *
+ * @param configName name of the new configuration. Has to be unique.The user will have to
+ * choose this name whenever he wants to apply this configuration to a beacon
+ */
+ private void saveCurrentConfigurationWithName(final String configName) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ BeaconConfiguration currentConfiguration = new BeaconConfiguration(configName);
+
+ GlobalFragment globalFragment = (GlobalFragment) slotsAdapter.getItem(0);
+
+ for (int i = 0; i < capabilities.getMaxSupportedTotalSlots(); i++) {
+ SlotFragment currFragment = (SlotFragment) slotsAdapter.getItem(i + 1);
+ if (!currFragment.isEmpty()) {
+ byte[] newSlotData = currFragment.buildNewSlotDataInfo();
+
+ String txPowerString;
+ String advPowerString;
+ String advIntervalString;
+ if (capabilities.isVariableTxPowerSupported()) {
+ txPowerString = currFragment.getTxPower();
+ advPowerString = currFragment.getAdvTxPower();
+ } else {
+ txPowerString = globalFragment.getTxPower();
+ advPowerString = globalFragment.getAdvTxPower();
+ }
+
+ if (capabilities.isVariableAdvSupported()) {
+ advIntervalString = currFragment.getAdvInterval();
+ } else {
+ advIntervalString = globalFragment.getAdvInterval();
+ }
+
+ currentConfiguration.addSlot(newSlotData, Integer.parseInt(txPowerString),
+ Integer.parseInt(advPowerString),
+ Integer.parseInt(advIntervalString));
+ }
+
+ }
+
+ configurationsManager.saveNewConfiguration(currentConfiguration);
+
+ enableDisplay();
+ UiUtils.showToast(BeaconConfigActivity.this,
+ "Configuration saved successfully");
+ }
+ });
+ }
+
+ /**
+ * Starts the process of applying a saved configuration to the current beacon. This pops up a
+ * dialog which shows the names of all saved configurations. The user can choose one of those
+ * and it will be applied and saved to current beacon
+ */
+ private void applyConfiguration() {
+ AlertDialog.Builder b = new AlertDialog.Builder(this);
+ b.setTitle("Apply Configuration");
+ final ArrayList configNames = configurationsManager.getConfigurationNamesList();
+ b.setItems(configNames.toArray(new String[configNames.size()]),
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ applyConfigurationWithName(configNames.get(which));
+ dialog.dismiss();
+ }
+ });
+ b.setOnCancelListener(new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialogInterface) {
+ enableDisplay();
+ }
+ });
+
+ b.show();
+
+
+ }
+
+ /**
+ * Applies the saved configuration with name "configName" to the current beacon
+ *
+ * @param configName the name of the configuration which the user wants to apply to the current
+ * beacon. It has to be the name of a configuration which the user has
+ * previously saved. If a configuration with this name does not exist, this
+ * method does nothing.
+ */
+ private void applyConfigurationWithName(final String configName) {
+ disableDisplay();
+
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ BeaconConfiguration config = configurationsManager.getConfiguration(configName);
+
+ if (config == null) {
+ return;
+ }
+
+ // These three are used only when per slot tx power and adv interval are not
+ // supported. They will record the highest value of the relevant variable
+ // out of all slots and then set that as the global value.
+ int maxAdvInterval = Integer.MIN_VALUE;
+ int maxTxPower = Integer.MIN_VALUE;
+ int maxAdvTxPower = Integer.MIN_VALUE;
+ for (int i = 0; i < capabilities.getMaxSupportedTotalSlots(); i++) {
+ SlotFragment slotFragment = (SlotFragment) slotsAdapter.getItem(i+1);
+ if (i >= config.getNumberOfConfiguredSlots()) {
+ // There are more available slots on this beacon than the saved
+ // configuration has defined. Configuring them as empty.
+ if (!slotFragment.isEmpty()) {
+ Log.d(TAG, "Configuring empty slot " + i);
+ slotDataChanged(i, new byte[0]);
+ }
+ continue;
+ }
+
+ slotDataChanged(i, config.getSlotDataForSlot(i));
+
+ if (capabilities.isVariableTxPowerSupported()) {
+ slotFragment.setTxPower(config.getRadioTxPowerForSlot(i));
+ slotFragment.setAdvTxPower(config.getAdvTxPowerForSlot(i));
+ } else {
+ if (config.getRadioTxPowerForSlot(i) > maxTxPower) {
+ maxTxPower = config.getRadioTxPowerForSlot(i);
+ }
+
+ if (config.getAdvTxPowerForSlot(i) > maxAdvTxPower) {
+ maxAdvTxPower = config.getAdvTxPowerForSlot(i);
+ }
+ }
+
+ if (capabilities.isVariableAdvSupported()) {
+ slotFragment.setAdvInterval(config.getAdvIntervalForSlot(i));
+ } else if (config.getAdvIntervalForSlot(i) > maxAdvInterval) {
+ maxAdvInterval = config.getAdvIntervalForSlot(i);
+ }
+ }
+
+ GlobalFragment globalFragment = (GlobalFragment) slotsAdapter.getItem(0);
+ if (!capabilities.isVariableTxPowerSupported()) {
+ globalFragment.setTxPower(maxTxPower);
+ globalFragment.setAdvTxPower(maxAdvTxPower);
+ }
+
+ if (!capabilities.isVariableAdvSupported()) {
+ globalFragment.setAdvInterval(maxAdvInterval);
+ }
+
+ saveChangesAcrossAllTabs();
+ }
+ });
+ }
+
+ /**
+ * Stops the swipeRefreshLayout from rotating (if it was rotating at the time of call) and
+ * disables all click events on the screen until a call to enableDisplay() is made.
+ *
+ * Runs on the UI thread
+ */
+ private void disableDisplay() {
+ swipeRefreshLayout.post(new Runnable() {
+ @Override
+ public void run() {
+ swipeRefreshLayout.setRefreshing(true);
+ }
+ });
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ findViewById(R.id.grey_out_slot).setVisibility(View.VISIBLE);
+ }
+ });
+ }
+
+ /**
+ * Enables the display after it has been previously disabled by disableDisplay()
+ *
+ * Runs on the UI thread
+ */
+ private void enableDisplay() {
+ swipeRefreshLayout.post(new Runnable() {
+ @Override
+ public void run() {
+ swipeRefreshLayout.setRefreshing(false);
+ }
+ });
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ findViewById(R.id.grey_out_slot).setVisibility(View.GONE);
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.menu_beacon_config, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ case R.id.save_changes:
+ saveChangesAcrossAllTabs();
+ return false;
+ case R.id.save_configuration:
+ saveCurrentConfiguration();
+ return false;
+ case R.id.apply_configuration:
+ applyConfiguration();
+ return false;
+ case R.id.manage_configurations:
+ Intent intent = new Intent(this, ManageConfigurationsActivity.class);
+ startActivity(intent);
+ return false;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ /**
+ * If the current beacon has unsaved changes and the user presses the back button, this will pop
+ * up a dialog asking whether to save the changes or not.
+ */
+ @Override
+ public void onBackPressed() {
+ if (tabChangesPending()) {
+ ConfirmationDialog.confirm("Save Changes", "There are some "
+ + "unsaved changes to this slot. \n \n Would you like to "
+ + "save those changes?", "SAVE", "DISCARD", BeaconConfigActivity.this,
+ new ConfirmationDialog.ConfirmationListener() {
+
+ @Override
+ public void confirm() {
+ saveChangesAcrossAllTabs();
+ intendedDisconnection = true;
+ BeaconConfigActivity.super.onBackPressed();
+ }
+
+ @Override
+ public void cancel() {
+ intendedDisconnection = true;
+ BeaconConfigActivity.super.onBackPressed();
+ }
+ });
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ /**
+ * @return true if there are changes to the ui which have not been written to the beacon
+ */
+ private boolean tabChangesPending() {
+ if (slotsAdapter != null) {
+ boolean changesPending = false;
+ for (int i = 0; i < slotsAdapter.getCount(); i++) {
+ changesPending = changesPending || slotsAdapter.getItem(i).changesPending();
+ }
+ return changesPending;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ gattClient.disconnect();
+ }
+
+ @Override
+ public void txPowerChanged(final int slot, final int txPower) {
+ try {
+ if (capabilities.isVariableTxPowerSupported()) {
+ gattClient.writeActiveSlot(slot);
+ }
+ gattClient.writeRadioTxPower(txPower);
+ int newlyReadTxPower = gattClient.readRadioTxPower()[0];
+ if (txPower == newlyReadTxPower) {
+ Bundle newTxPowerBundle = new Bundle();
+ newTxPowerBundle.putString(Constants.TX_POWER, Integer.toString(newlyReadTxPower));
+ slotsAdapter.getItem(slot + 1).updateInformation(newTxPowerBundle);
+ Log.d(TAG, "Radio Tx Power changed to " + newlyReadTxPower);
+ }
+ } catch (GattClientException e) {
+ Log.d(TAG, "Gatt Client Error when writing radio tx power to beacon", e);
+ displayConnectionFailScreen("Something went wrong when writing tx power to beacon");
+ } catch (GattOperationException e) {
+ Log.d(TAG, "Gatt Operation Error when writing tx power to beacon", e);
+ displayConnectionFailScreen("Something went wrong when writing tx power to beacon");
+ }
+ }
+
+ @Override
+ public void advTxPowerChanged(final int slot, final int advTxPower) {
+ try {
+ if (capabilities.isVariableTxPowerSupported()) {
+ gattClient.writeActiveSlot(slot);
+ }
+ gattClient.writeAdvertisedTxPower(advTxPower);
+ int newlyReadAdvTx = gattClient.readAdvertisedTxPower()[0];
+ if (advTxPower == newlyReadAdvTx) {
+ Bundle newAdvTxBundle = new Bundle();
+ newAdvTxBundle.putString(Constants.ADV_POWER, Integer.toString(newlyReadAdvTx));
+ slotsAdapter.getItem(slot + 1).updateInformation(newAdvTxBundle);
+ Log.d(TAG, "Advertised Tx Power changed to " + newlyReadAdvTx);
+ }
+ } catch (GattClientException e) {
+ Log.d(TAG, "Gatt Client Error when adv tx power to beacon", e);
+ displayConnectionFailScreen("Something went wrong when writing adv tx power to beacon");
+ } catch (GattOperationException e) {
+ Log.d(TAG, "Gatt Operation Error when writing adv tx power to beacon", e);
+ displayConnectionFailScreen("Something went wrong when writing adv tx power to beacon");
+ }
+ }
+
+ @Override
+ public void advIntervalChanged(final int slot, final int advInterval) {
+ final BeaconTabFragment currentFragment = slotsAdapter.getItem(slot + 1);
+ try {
+ if (capabilities.isVariableAdvSupported()) {
+ gattClient.writeActiveSlot(slot);
+ }
+ gattClient.writeAdvertisingInterval(advInterval);
+ int newlyReadAdvInt = Utils.toInt(gattClient.readAdvertisingInterval());
+ if (advInterval == newlyReadAdvInt) {
+ Bundle newAdvIntBundle = new Bundle();
+ newAdvIntBundle.putString(Constants.ADV_INTERVAL,
+ Integer.toString(newlyReadAdvInt));
+ currentFragment.updateInformation(newAdvIntBundle);
+ Log.d(TAG, "Advertising interval changed to " + advInterval);
+ }
+ } catch (GattClientException e) {
+ Log.d(TAG, "Gatt Client Exception when writing adv interval to beacon", e);
+ displayConnectionFailScreen("Something went wrong when writing adv interval to beacon");
+ } catch (GattOperationException e) {
+ Log.d(TAG, "Gatt Operation Error when writing adv interval to beacon", e);
+ displayConnectionFailScreen("Something went wrong when writing adv interval to beacon");
+ }
+ }
+
+ @Override
+ public void slotDataChanged(final int slot, final byte[] newSlotData) {
+ try {
+ Log.d(TAG, "New slot data: " + Arrays.toString(newSlotData));
+ gattClient.writeActiveSlot(slot);
+ gattClient.writeAdvSlotData(newSlotData);
+ byte[] newlyReadSlotData = gattClient.readAdvSlotData();
+ Bundle newSlotDataBundle = new Bundle();
+ changeTabName(slot + 1, newlyReadSlotData);
+ newSlotDataBundle.putByteArray(Constants.SLOT_DATA, newlyReadSlotData);
+ slotsAdapter.getItem(slot + 1).updateInformation(newSlotDataBundle);
+ } catch (GattClientException e) {
+ Log.d(TAG, "Gatt Client Error when writing slot data to beacon", e);
+ displayConnectionFailScreen("Something went wrong when writing slot data to beacon");
+ } catch (GattOperationException e) {
+ Log.d(TAG, "Gatt Operation Error when writing slot data to beacon", e);
+ displayConnectionFailScreen("Something went wrong when writing slot data to beacon");
+ }
+ }
+
+ private void changeTabName(final int tabNo, final byte[] slotData) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ TabLayout.Tab tab = ((TabLayout) findViewById(R.id.tabs)).getTabAt(tabNo);
+ if (Utils.slotIsEmpty(slotData)) {
+ tab.setText("--");
+ } else {
+ tab.setText(Utils.getStringFromFrameType(slotData[0]));
+ }
+ }
+ });
+ }
+
+ @Override
+ public void lockCodeChanged(final String newLockCode) {
+ byte[] newCodeBytes = Utils.toByteArray(newLockCode);
+ byte[] encryptedCode = Utils.aes128Encrypt(newCodeBytes, Utils.toByteArray(unlockCode));
+
+ byte[] newLockCodeBytes = new byte[17];
+ newLockCodeBytes[0] = 0;
+ newLockCodeBytes = Utils.rewriteBytes(newLockCodeBytes, 1, 16, encryptedCode);
+ try {
+ gattClient.writeLockState(newLockCodeBytes);
+ gattClient.unlock(newCodeBytes);
+ UiUtils.showToast(BeaconConfigActivity.this, "Lock code changed successfully");
+ } catch (GattClientException e) {
+ Log.d(TAG, "Gatt Client Error when changing lock code of beacon", e);
+ displayConnectionFailScreen("Something went wrong when changing lock code of beacon");
+ } catch (GattOperationException e) {
+ Log.d(TAG, "Gatt Operation Error when changing lock code of beacon", e);
+ displayConnectionFailScreen("Something went wrong when changing lock code of beacon");
+ }
+ }
+
+ @Override
+ public void factoryResetCalled() {
+ disableDisplay();
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ byte[] factoryResetCode = Utils.toByteArray("0b");
+ gattClient.writeFactoryReset(factoryResetCode);
+ setupBeaconInformationDisplay();
+ // setupBeaconInformationDisplay() will enable display after execution
+ UiUtils.showToast(BeaconConfigActivity.this, "Beacon was reset successfully");
+ } catch (GattClientException e) {
+ Log.d(TAG, "Gatt Client Error when factory resetting beacon", e);
+ displayConnectionFailScreen("Something went wrong when resetting beacon");
+ } catch (GattOperationException e) {
+ Log.d(TAG, "Gatt Operation Error when factory resetting beacon", e);
+ displayConnectionFailScreen("Something went wrong when resetting beacon");
+ }
+ }
+ });
+
+ }
+
+ @Override
+ public void remainConnectableChanged(final boolean remainConnectable) {
+ try {
+ byte[] remainConnectableBytes = new byte[1];
+ remainConnectableBytes[0] = (byte) (remainConnectable ? 1 : 0);
+ gattClient.writeRemainConnectable(remainConnectableBytes);
+ } catch (GattClientException e) {
+ Log.d(TAG, "Gatt Client Error when writing remain connectable to beacon", e);
+ displayConnectionFailScreen("Something went wrong when writing remain connectable "
+ + "to beacon");
+ } catch (GattOperationException e) {
+ Log.d(TAG, "Gatt Operation Error when writing remain connectable to beacon", e);
+ displayConnectionFailScreen("Something went wrong when writing remain connectable "
+ + "to beacon");
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconConfiguration.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconConfiguration.java
new file mode 100644
index 0000000..ba21501
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconConfiguration.java
@@ -0,0 +1,90 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**Keeps one configuration of a beacon. This includes all the slots specific
+ * information, tx power, adv tx power and advertised interval. This class only holds
+ * slot information about UID, URL and TLM frames. EID frames will not be saved.
+ */
+public class BeaconConfiguration {
+ private String configurationName;
+ private List slots;
+
+ public BeaconConfiguration(String configurationName) {
+ this.configurationName = configurationName;
+ slots = new ArrayList<>();
+ }
+
+ /**
+ * Saved this information about a beacon's slot. It does not save EID configured slots.
+ *
+ * @param slotData slotData of this slot of the configuration
+ * @param txPower radio tx power of this slot of the configuration
+ * @param advTxPower advertised tx power of this slot of the configuration
+ * @param advInterval advertised interval of this slot of the configuration
+ */
+ public void addSlot(byte[] slotData, int txPower, int advTxPower, int advInterval) {
+ if (slotData[0] == Constants.EID_FRAME_TYPE) {
+ return;
+ }
+ slots.add(new Slot(slotData, txPower, advTxPower, advInterval));
+ }
+
+ public String getName() {
+ return configurationName;
+ }
+
+ public int getNumberOfConfiguredSlots() {
+ return slots.size();
+ }
+
+ public byte[] getSlotDataForSlot(int slotNumber) {
+ return slots.get(slotNumber).slotData;
+ }
+
+ public int getRadioTxPowerForSlot(int slotNumber) {
+ return slots.get(slotNumber).txPower;
+ }
+
+ public int getAdvTxPowerForSlot(int slotNumber) {
+ return slots.get(slotNumber).advTxPower;
+ }
+
+ public int getAdvIntervalForSlot(int slotNumber) {
+ return slots.get(slotNumber).advInterval;
+ }
+
+ public void setName(String newConfigurationName) {
+ this.configurationName = newConfigurationName;
+ }
+
+ private class Slot {
+ byte[] slotData;
+ int txPower;
+ int advTxPower;
+ int advInterval;
+
+ Slot(byte[] slotData, int txPower, int advTxPower, int advInterval) {
+ this.slotData = slotData;
+ this.txPower = txPower;
+ this.advTxPower = advTxPower;
+ this.advInterval = advInterval;
+ }
+ }
+
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconListAdapter.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconListAdapter.java
new file mode 100644
index 0000000..faae48f
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconListAdapter.java
@@ -0,0 +1,224 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.drawable.GradientDrawable;
+import android.support.v7.widget.CardView;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.github.google.beaconfig.utils.SlotDataManager;
+import com.github.google.beaconfig.utils.Utils;
+
+import java.util.List;
+
+/**
+ * Adapter for the recyclerView displaying all the results from scanning for beacons nearby in
+ * ScanningActivity. Each list entry is a CardView whose structure changes depending on the
+ * information available from the scan result. It is inflated for every frame which the beacon
+ * is broadcasting with 1 row which displays this information.
+ */
+
+public class BeaconListAdapter extends RecyclerView.Adapter {
+ private List scanDataList;
+ private Context context;
+
+ public BeaconListAdapter(List scanDataList, Context context) {
+ this.scanDataList = scanDataList;
+ this.context = context;
+ }
+
+ @Override
+ public BeaconListAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.scan_result_layout, parent, false);
+ ViewHolder vh = new ViewHolder(v);
+ return vh;
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ BeaconScanData b = scanDataList.get(position);
+ holder.name.setText(b.name);
+ holder.address.setText(b.deviceAddress);
+ holder.distance.setText(Integer.toString(b.rssi));
+
+ holder.bottom.removeAllViews();
+ boolean uidPresent = false;
+ for (byte[] uidFrameType : b.uidFrameTypes) {
+ uidPresent = setupUidFrameView(uidFrameType, holder);
+ }
+ boolean urlPresent = false;
+ for (byte[] urlFrameType : b.urlFrameTypes) {
+ urlPresent = setupUrlFrameView(urlFrameType, holder);
+ }
+ boolean tlmPresent = setupTlmFrameView(b.tlmFrameType, holder);
+ boolean eidPresent = setupEidFrameView(b.eidFrameType, holder);
+
+ //This part is making sure that "Device is configurable" is displayed only when there is
+ // no frame data available to display. The card looks very empty is there wasn't a
+ // message like that.
+ if (b.connectable) {
+ ((GradientDrawable) holder.connectable.getBackground()).setColor(
+ context.getResources().getColor(R.color.colorPrimary));
+ if (!(uidPresent || urlPresent || tlmPresent || eidPresent)) {
+ holder.bottom.addView(holder.connectableRow);
+ }
+ } else {
+ ((GradientDrawable) holder.connectable.getBackground()).setColor(
+ context.getResources().getColor(R.color.red));
+ }
+ }
+
+ private boolean setupUidFrameView(byte[] data, ViewHolder holder) {
+ if (data != null) {
+ LayoutInflater inflater = holder.inflater;
+ LinearLayout row = (LinearLayout) inflater.inflate(R.layout.frame_row_uid, null);
+ TextView namespaceView = (TextView) row.findViewById(R.id.namespace);
+ namespaceView.setText(SlotDataManager.getNamespaceFromSlotData(data));
+ TextView instanceView = (TextView) row.findViewById(R.id.instance);
+ instanceView.setText(SlotDataManager.getInstanceFromSlotData(data));
+ holder.bottom.addView(row);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean setupUrlFrameView(byte[] data, ViewHolder holder) {
+ if (data != null) {
+ LayoutInflater inflater = holder.inflater;
+ LinearLayout row = (LinearLayout) inflater.inflate(R.layout.frame_row_url, null);
+ TextView serviceDataView = (TextView) row.findViewById(R.id.url);
+ serviceDataView.setText(SlotDataManager.getUrlFromSlotData(data));
+ holder.bottom.addView(row);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean setupTlmFrameView(byte[] data, ViewHolder holder) {
+ if (data != null) {
+ String voltage = Short.toString(SlotDataManager.getVersionFromSlotData(data));
+
+ String temperature = Float.toString(SlotDataManager.getTemperatureFromSlotData(data));
+
+ String advCnt
+ = Integer.toString(SlotDataManager.getAdvertisingPDUCountFromSlotData(data));
+
+ String timeOn = Utils.getTimeString(SlotDataManager.getTimeSinceOnFromSlotData(data));
+
+ LayoutInflater inflater = holder.inflater;
+ LinearLayout row = (LinearLayout) inflater.inflate(R.layout.frame_row_tlm, null);
+ TextView voltageView = (TextView) row.findViewById(R.id.voltage);
+ voltageView.setText(voltage);
+
+ TextView temperatureView = (TextView) row.findViewById(R.id.temperature);
+ temperatureView.setText(temperature);
+ TextView pduView = (TextView) row.findViewById(R.id.pdu_cnt);
+ pduView.setText(advCnt);
+
+ TextView timeOnView = (TextView) row.findViewById(R.id.time_on);
+ timeOnView.setText(timeOn);
+ holder.bottom.addView(row);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean setupEidFrameView(byte[] data, ViewHolder holder) {
+ if (data != null) {
+ LayoutInflater inflater = holder.inflater;
+ LinearLayout row = (LinearLayout) inflater.inflate(R.layout.frame_type_row, null);
+ ((TextView) row.findViewById(R.id.frame_type)).setText(Constants.EID);
+ TextView serviceDataView = (TextView) row.findViewById(R.id.service_data);
+ serviceDataView.setText(SlotDataManager.getEphemeralIdFromSlotData(data));
+ holder.bottom.addView(row);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public int getItemCount() {
+ return scanDataList.size();
+ }
+
+ public void setData(List scanData) {
+ this.scanDataList = scanData;
+ notifyDataSetChanged();
+ }
+
+ /**
+ * View holder for each displaying card in the recycler view.
+ */
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+ public static final String BEACON_ADDRESS = "com.bluetooth.beaconconfig.BEACON_ADDRESS";
+ public static final String BEACON_NAME = "com.bluetooth.beaconconfig.BEACON_NAME";
+
+ private CardView cardView;
+ LinearLayout bottom;
+
+ private TextView name;
+ private TextView address;
+ private TextView distance;
+
+ LayoutInflater inflater;
+
+ View connectable;
+
+ private LinearLayout connectableRow;
+
+ public ViewHolder(View itemView) {
+ super(itemView);
+ this.cardView = (CardView) itemView.findViewById(R.id.card_view);
+ cardView.setUseCompatPadding(true);
+
+ cardView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Intent configBeacon =
+ new Intent(cardView.getContext(), BeaconConfigActivity.class);
+ configBeacon.putExtra(BEACON_ADDRESS, address.getText());
+ configBeacon.putExtra(BEACON_NAME, name.getText());
+ cardView.getContext().startActivity(configBeacon);
+ }
+ });
+
+ name = (TextView) cardView.findViewById(R.id.name);
+ address = (TextView) cardView.findViewById(R.id.address);
+ distance = (TextView) cardView.findViewById(R.id.rssi);
+
+ bottom = (LinearLayout) itemView.findViewById(R.id.bottom);
+ inflater = (LayoutInflater) cardView.getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ connectable = cardView.findViewById(R.id.connectable);
+ connectableRow = (LinearLayout) inflater.inflate(R.layout.frame_type_row, null);
+ connectableRow.removeAllViews();
+ TextView textView = new TextView(connectableRow.getContext());
+ textView.setText("Device is configurable.");
+ textView.setTextColor(Color.GRAY);
+ connectableRow.addView(textView);
+ }
+
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconScanData.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconScanData.java
new file mode 100644
index 0000000..420167b
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconScanData.java
@@ -0,0 +1,103 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig;
+
+import android.bluetooth.le.ScanResult;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import com.github.google.beaconfig.utils.Utils;
+
+/**
+ * Holder for all the data that the scanner from the Scanning Activity receives about a particular
+ * beacon. This data will then be displayed in the recyclerView.
+ */
+
+public class BeaconScanData {
+ public static final String TAG = BeaconScanData.class.getSimpleName();
+
+ final String deviceAddress;
+ int rssi;
+ String name;
+ boolean connectable = false;
+
+ final List uidFrameTypes;
+ byte[] tlmFrameType;
+ final List urlFrameTypes;
+ byte[] eidFrameType;
+
+ BeaconScanData(ScanResult sr) {
+ this.deviceAddress = sr.getDevice().getAddress();
+ if (sr.getDevice().getName() != null) {
+ name = sr.getDevice().getName();
+ } else {
+ name = "[no name]";
+ }
+ uidFrameTypes = new ArrayList<>();
+ urlFrameTypes = new ArrayList<>();
+
+ update(sr);
+ }
+
+ public void update(ScanResult sr) {
+ rssi = sr.getRssi();
+ byte[] serviceData = sr.getScanRecord().getServiceData(Constants.EDDYSTONE_SERVICE_UUID);
+ if (serviceData == null || serviceData.length == 0) {
+ serviceData = sr.getScanRecord().getServiceData(Constants.EDDYSTONE_CONFIGURATION_UUID);
+ if (serviceData == null || Utils.slotIsEmpty(serviceData)) {
+ connectable = true;
+ return;
+ } else {
+ String err = "No suitable service data.";
+ Log.d(TAG, err);
+ }
+ }
+
+ switch (serviceData[0]) {
+ case Constants.UID_FRAME_TYPE:
+ synchronized (uidFrameTypes) {
+ for (byte[] uidServiceData : uidFrameTypes) {
+ if (Arrays.equals(serviceData, uidServiceData)) {
+ return;
+ }
+ }
+ uidFrameTypes.add(serviceData);
+ }
+ break;
+ case Constants.URL_FRAME_TYPE:
+ synchronized (urlFrameTypes) {
+ for (byte[] urlServiceData : urlFrameTypes) {
+ if (Arrays.equals(serviceData, urlServiceData)) {
+ return;
+ }
+ }
+ urlFrameTypes.add(serviceData);
+ }
+ break;
+ case Constants.TLM_FRAME_TYPE:
+ tlmFrameType = serviceData;
+ break;
+ case Constants.EID_FRAME_TYPE:
+ eidFrameType = serviceData;
+ break;
+ default:
+ String err = String.format("Invalid frame type byte %02X", serviceData[0]);
+ Log.d(TAG, err);
+ }
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconScanner.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconScanner.java
new file mode 100644
index 0000000..b44c926
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/BeaconScanner.java
@@ -0,0 +1,135 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig;
+
+import android.app.Activity;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.le.BluetoothLeScanner;
+import android.bluetooth.le.ScanCallback;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanResult;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import com.github.google.beaconfig.utils.UiUtils;
+
+/**
+ * Scanner class using the BluetoothLeScanner to scan for beacons and convoy the result in a
+ * ready-to-use way to the scanningActivity.
+ *
+ * It uses a list of scan filters to filter through the scan results and saves each good scan result
+ * in a BeaconScanData structure. All the results are stored in a List which is sent back to the
+ * ScanningActivity to display in the UI.
+ *
+ * ScanResults from the same beacon are used to update the same BeaconScanData object, so that in
+ * the end each BeaconScanData corresponds to a unique beacon.
+ */
+class BeaconScanner {
+ private static final String TAG = BeaconScanner.class.getSimpleName();
+
+ private Activity currentActivity;
+ private BluetoothLeScanner scanner;
+ private ScanCallback scanCallback;
+ private List scanFilters;
+ final private List scanDataList;
+ final private Map addrToBeacons;
+
+ public BeaconScanner(final Activity currentActivity, BluetoothAdapter bluetoothAdapter) {
+ this.currentActivity = currentActivity;
+ scanner = bluetoothAdapter.getBluetoothLeScanner();
+
+ scanCallback = new ScanCallback() {
+ @Override
+ public void onScanResult(int callbackType, ScanResult result) {
+ BeaconScanData beaconScanData;
+ String address = result.getDevice().getAddress();
+ synchronized (addrToBeacons) {
+ if (addrToBeacons.isEmpty() || !addrToBeacons.containsKey(address)) {
+ beaconScanData = new BeaconScanData(result);
+
+ addrToBeacons.put(address, beaconScanData);
+ scanDataList.add(beaconScanData);
+ } else {
+ beaconScanData = addrToBeacons.get(address);
+ beaconScanData.update(result);
+ }
+ }
+ }
+
+ @Override
+ public void onScanFailed(int errorCode) {
+ switch (errorCode) {
+ case SCAN_FAILED_ALREADY_STARTED:
+ Log.d(TAG, "SCAN_FAILED_ALREADY_STARTED");
+ break;
+ case SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
+ Log.d(TAG, "SCAN_FAILED_APPLICATION_REGISTRATION_FAILED");
+ UiUtils.showToast(currentActivity, "Scan failed. Try to turn bluetooth " +
+ "OFF and ON again");
+ break;
+ case SCAN_FAILED_FEATURE_UNSUPPORTED:
+ Log.d(TAG, "SCAN_FAILED_FEATURE_UNSUPPORTED");
+ break;
+ case SCAN_FAILED_INTERNAL_ERROR:
+ Log.d(TAG, "SCAN_FAILED_INTERNAL_ERROR");
+ break;
+ default:
+ Log.d(TAG, "Scan failed, unknown error code");
+ break;
+ }
+ }
+ };
+
+ scanFilters = new ArrayList<>();
+ scanFilters.add(new ScanFilter.Builder().setServiceUuid
+ (Constants.EDDYSTONE_SERVICE_UUID).build());
+ scanFilters.add(new ScanFilter.Builder().setServiceUuid
+ (Constants.EDDYSTONE_CONFIGURATION_UUID).build());
+ addrToBeacons = new HashMap<>();
+ scanDataList = new ArrayList<>();
+ }
+
+ private boolean isScanning = false;
+ public void scan() {
+ if (isScanning) {
+ return;
+ }
+ isScanning = true;
+ scanDataList.clear();
+ addrToBeacons.clear();
+ scanner.startScan(scanFilters, Constants.SCAN_SETTINGS, scanCallback);
+ final ScheduledExecutorService worker =
+ Executors.newSingleThreadScheduledExecutor();
+ Runnable stop = new Runnable() {
+ public void run() {
+ synchronized (scanDataList) {
+ scanner.stopScan(scanCallback);
+ isScanning = false;
+ ((ScanningActivity) currentActivity).scanComplete(scanDataList);
+ }
+ }
+ };
+
+ worker.schedule(stop, Constants.SCAN_TIME_SECS, TimeUnit.SECONDS);
+ }
+
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/Constants.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/Constants.java
new file mode 100644
index 0000000..a51adc8
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/Constants.java
@@ -0,0 +1,74 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig;
+
+import android.bluetooth.le.ScanSettings;
+import android.os.ParcelUuid;
+
+/**
+ * Constants used by other classes.
+ */
+
+public class Constants {
+ static final int SCAN_TIME_SECS = 4;
+ static final int REQUEST_ENABLE_BLUETOOTH = 1;
+ static final ScanSettings SCAN_SETTINGS =
+ new ScanSettings.Builder()
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+ .setReportDelay(0)
+ .build();
+ static final ParcelUuid EDDYSTONE_SERVICE_UUID =
+ ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
+ public static final ParcelUuid EDDYSTONE_CONFIGURATION_UUID =
+ ParcelUuid.fromString("a3c87500-8ed3-4bdf-8a39-a01bebede295");
+
+ /**
+ * Eddystone-UID frame type value.
+ */
+ public static final byte UID_FRAME_TYPE = 0x00;
+
+ /**
+ * Eddystone-URL frame type value.
+ */
+ public static final byte URL_FRAME_TYPE = 0x10;
+
+ /**
+ * Eddystone-TLM frame type value.
+ */
+ public static final byte TLM_FRAME_TYPE = 0x20;
+
+ /**
+ * Eddystone-EID frame type value.
+ */
+ public static final byte EID_FRAME_TYPE = 0x30;
+ public static final byte EMPTY_FRAME_TYPE = 0x40;
+
+ public static final String SLOT_DATA = "slot_data";
+ public static final String TX_POWER = "tx_power";
+ public static final String ADV_POWER = "adv_power";
+ public static final String UID = "UID";
+ public static final String URL = "URL";
+ public static final String TLM = "TLM";
+ public static final String EID = "EID";
+ public static final String ADV_INTERVAL = "advertising_interval";
+ public static final String BEACON_ADDRESS = "beacon_address";
+ public static final String BEACON_NAME = "beacon_name";
+ public static final String BROADCAST_CAPABILITIES = "BROADCAST_CAPABILITIES";
+ public static final String SLOT_NUMBER = "SLOT_NUMBER";
+ public static final String REMAIN_CONNECTABLE = "REMAIN_CONNECTABLE";
+
+ public static final String CONFIG_NAMES = "CONFIG_NAMES";
+ public static final String SAVED_CONFIGURATIONS = "SAVED_CONFIGURATIONS";
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/ManageConfigurationsActivity.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/ManageConfigurationsActivity.java
new file mode 100644
index 0000000..92b7381
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/ManageConfigurationsActivity.java
@@ -0,0 +1,68 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This activity shows all the configurations that the user has saved from different beacons.
+ *
+ * Configurations are presented in a RecyclerListView with each entry presented in a CardView.
+ * Each entry has a delete button and can be clicked to extend the cardView and show all the
+ * settings of this configuration.
+ *
+ * The back button navigates to the BeaconConfigActivity from which this activity was opened.
+ */
+public class ManageConfigurationsActivity extends AppCompatActivity {
+ private List configurations;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.fragment_manage_configurations);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getWindow().setStatusBarColor(getResources().getColor(R.color.colorPrimary));
+ setTitle("Manage configurations");
+
+ RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list);
+ recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ configurations = new ArrayList<>();
+ recyclerView.setAdapter(new ManageConfigurationsAdapter(this));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/ManageConfigurationsAdapter.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/ManageConfigurationsAdapter.java
new file mode 100644
index 0000000..5c9c6aa
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/ManageConfigurationsAdapter.java
@@ -0,0 +1,210 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig;
+
+import android.support.v7.widget.CardView;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.github.google.beaconfig.dialogs.ConfirmationDialog;
+import com.github.google.beaconfig.dialogs.SaveConfigurationDialog;
+import com.github.google.beaconfig.utils.SlotDataManager;
+
+/**
+ * Adapter used for the RecyclerView which presents the saved configurations that the user has saved
+ * from all beacons. It is shows inside ManageConfigurationsActivity. Each list entry presents one
+ * saved configuration.
+ */
+public class ManageConfigurationsAdapter
+ extends RecyclerView.Adapter {
+
+ private static final String TAG = ManageConfigurationsAdapter.class.getSimpleName();
+ private final ManageConfigurationsActivity activity;
+ private final SavedConfigurationsManager configurationsManager;
+
+ public ManageConfigurationsAdapter(ManageConfigurationsActivity activity) {
+ this.activity = activity;
+ configurationsManager = new SavedConfigurationsManager(activity);
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.fragment_manage_configurations_item, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(final ViewHolder holder, final int position) {
+ final BeaconConfiguration configuration
+ = configurationsManager.getConfigurationAtPosition(position);
+ holder.configNameView.setText(configuration.getName());
+
+ holder.deleteView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ ConfirmationDialog.confirm("Delete configuration", "Are you sure you want to "
+ + "delete configuration \"" + configuration.getName() + "\"?", "yes", "no",
+ activity, new ConfirmationDialog.ConfirmationListener() {
+ @Override
+ public void confirm() {
+ deleteConfiguration(configuration.getName());
+ }
+
+ @Override
+ public void cancel() {
+
+ }
+ });
+ }
+ });
+
+ // on click the card inflates to show detailed information about this configuration
+ holder.cardView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ holder.clickCard();
+ holder.contentView.removeAllViews();
+
+ LayoutInflater inflater = activity.getLayoutInflater();
+ for (int i = 0; i < configuration.getNumberOfConfiguredSlots(); i++) {
+ Log.d(TAG, "Printing slot " + i);
+ View child = inflater.inflate(R.layout.saved_slot_configuration, null);
+ holder.contentView.addView(child);
+
+ ((TextView) child.findViewById(R.id.header)).setText("Slot " + i + ": ");
+ setUpSlotDataConfigurationViews(child, configuration.getSlotDataForSlot(i));
+ ((TextView) child.findViewById(R.id.radio_tx_power))
+ .setText(Integer.toString(configuration.getRadioTxPowerForSlot(i)));
+ ((TextView) child.findViewById(R.id.adv_tx_power))
+ .setText(Integer.toString(configuration.getAdvTxPowerForSlot(i)));
+ ((TextView) child.findViewById(R.id.adv_interval))
+ .setText(Integer.toString(configuration.getAdvIntervalForSlot(i)));
+ }
+ }
+ });
+
+ //long click opens a window to change the name of this configuration
+ holder.cardView.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View view) {
+ SaveConfigurationDialog.show(activity,
+ new SaveConfigurationDialog.SaveConfigListener() {
+ @Override
+ public void configNameChosen(String configName) {
+ deleteConfiguration(configuration.getName());
+ configuration.setName(configName);
+ new SavedConfigurationsManager(activity)
+ .saveNewConfiguration(configuration);
+ notifyDataSetChanged();
+ }
+ });
+ return false;
+ }
+ });
+ }
+
+ private void setUpSlotDataConfigurationViews(View view, byte[] slotData) {
+ TextView header = (TextView) view.findViewById(R.id.header);
+ switch (slotData[0]) {
+ case Constants.UID_FRAME_TYPE:
+ header.setText(header.getText() + Constants.UID);
+ ((TextView) view.findViewById(R.id.namespace))
+ .setText(SlotDataManager.getNamespaceFromWriteByteArray(slotData));
+ ((TextView) view.findViewById(R.id.instance))
+ .setText(SlotDataManager.getInstanceFromWriteByteArray(slotData));
+ view.findViewById(R.id.uid_config).setVisibility(View.VISIBLE);
+ break;
+ case Constants.URL_FRAME_TYPE:
+ header.setText(header.getText() + Constants.URL);
+ ((TextView) view.findViewById(R.id.url))
+ .setText(SlotDataManager.getUrlFromWriteByteArray(slotData));
+ view.findViewById(R.id.url_config).setVisibility(View.VISIBLE);
+ break;
+ case Constants.TLM_FRAME_TYPE:
+ header.setText(header.getText() + Constants.TLM);
+ break;
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return configurationsManager.getNumberOfConfigurations();
+ }
+
+ private void deleteConfiguration(String name) {
+ configurationsManager.deleteConfigurationWithName(name);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Holder for a single configuration inside the list of saved configurations. It shows the
+ * name of the configuration and on click, shows the configuration in detail:
+ * - number of configured slots
+ * - frame that each slot is configured to
+ * - slot data
+ * - radio tx power
+ * - advertised tx power
+ * advertised interval
+ *
+ * There is also an option to delete this list entry by clicking on the deleteView.
+ */
+ public class ViewHolder extends RecyclerView.ViewHolder {
+ public CardView cardView;
+ public final TextView configNameView;
+ public final ImageView deleteView;
+ public final LinearLayout contentView;
+
+ public ViewHolder(final View itemView) {
+ super(itemView);
+ configNameView = (TextView) itemView.findViewById(R.id.config_name);
+ deleteView = (ImageView) itemView.findViewById(R.id.config_delete);
+ contentView = (LinearLayout) itemView.findViewById(R.id.config_content);
+
+ cardView = (CardView) itemView.findViewById(R.id.configuration_item_card);
+ cardView.setUseCompatPadding(true);
+
+ final ImageView expandArrow
+ = (ImageView) itemView.findViewById(R.id.expand_configuration);
+ expandArrow.setOnClickListener(new View.OnClickListener() {
+ private boolean expanded = false;
+ @Override
+ public void onClick(View view) {
+ if (expanded) {
+ itemView.findViewById(R.id.configuration_view).setVisibility(View.GONE);
+ expandArrow.setImageResource(R.drawable.expand_arrow);
+ expanded = false;
+ } else {
+ itemView.findViewById(R.id.configuration_view).setVisibility(View.VISIBLE);
+ expandArrow.setImageResource(R.drawable.close_arrow);
+ expanded = true;
+ }
+
+ }
+ });
+ }
+
+ public void clickCard() {
+ cardView.findViewById(R.id.expand_configuration).performClick();
+ }
+
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/SavedConfigurationsManager.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/SavedConfigurationsManager.java
new file mode 100644
index 0000000..492eae6
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/SavedConfigurationsManager.java
@@ -0,0 +1,165 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import com.google.gson.Gson;
+
+import java.util.ArrayList;
+
+/**
+ * This class is used from the BeaconConfig activity for saving the beacon's current configuration.
+ * It uses SharedPreferences to save or recover beacon configurations.
+ *
+ * It accepts a BeaconConfiguration object to save a configuration and serializes it to a String
+ * which it puts in the SharedPreferences.
+ *
+ * It returns a BeaconConfiguration object when a configuration with a certain name is requested.
+ */
+public class SavedConfigurationsManager {
+ private static final String TAG = SavedConfigurationsManager.class.getSimpleName();
+ private final Activity activity;
+ private final Gson gson;
+
+ public SavedConfigurationsManager(Activity activity) {
+ this.activity = activity;
+ this.gson = new Gson();
+ }
+
+ /**
+ * This function must be called before any other call to this class. It initialises the list of
+ * configuration names
+ */
+ public void initialiseConfigurationSaving() {
+ ArrayList configurationNames = new ArrayList<>();
+ SharedPreferences.Editor spe = activity.getSharedPreferences(Constants.SAVED_CONFIGURATIONS,
+ Context.MODE_PRIVATE).edit();
+ spe.putString(Constants.CONFIG_NAMES, gson.toJson(configurationNames));
+ spe.apply();
+ }
+
+ /**
+ * This method serializes the BeaconConfiguration object to a String which it puts in a shared
+ * preferences file with name "SAVED_CONFIGURATIONS". The new configuration is saved with key
+ * the name of the configuration (configuration.getName()).
+ *
+ * @param configuration the configuration which we want to save
+ */
+ public void saveNewConfiguration(BeaconConfiguration configuration) {
+ SharedPreferences.Editor spEditor = activity.getSharedPreferences(
+ Constants.SAVED_CONFIGURATIONS, Context.MODE_PRIVATE).edit();
+
+ String serializedConfiguration = gson.toJson(configuration);
+ spEditor.putString(configuration.getName(), serializedConfiguration);
+ addNewName(configuration.getName());
+ spEditor.apply();
+ }
+
+ /**
+ * At the beginning of the application an array was put in the "SAVED_CONFIGURATIONS" shared
+ * preferences file which is a list of all saved configurations' names. This method adds a new
+ * name to this list.
+ *
+ * This method should only be called when a new configuration is saved to the shared preferences
+ * file and the configNameView is the key with which the configuration was saved in the shared
+ * preferences map.
+ *
+ * @param configName
+ */
+ private void addNewName(String configName) {
+ ArrayList spConfigNames = getConfigurationNamesList();
+ spConfigNames.add(configName);
+
+ SharedPreferences.Editor spe = activity.getSharedPreferences(Constants.SAVED_CONFIGURATIONS,
+ Context.MODE_PRIVATE).edit();
+ spe.remove(Constants.CONFIG_NAMES);
+ spe.putString(Constants.CONFIG_NAMES, gson.toJson(spConfigNames));
+ spe.apply();
+ }
+
+ /**
+ * Retrieves a configuration from the "SAVED_CONFIGURATIONS" shared preferences file with name
+ * configNameView. This configuration must have previously been saved to the shared preferences
+ * file
+ *
+ * @param configName the name of the configuration we want to retrieve
+ * @return the saved configuration with this name or null if no such configuration was found
+ */
+ public BeaconConfiguration getConfiguration(String configName) {
+ SharedPreferences sp = activity.getSharedPreferences(
+ Constants.SAVED_CONFIGURATIONS, Context.MODE_PRIVATE);
+ String serializedConfiguration = sp.getString(configName, "");
+ BeaconConfiguration configuration
+ = gson.fromJson(serializedConfiguration, BeaconConfiguration.class);
+
+ if (configuration == null) {
+ Log.d(TAG, "Configuration \"" + configName + "\" not found");
+ return null;
+ }
+ Log.d(TAG, "Retrieved configuration " + configuration.getName());
+ return configuration;
+ }
+
+ /**
+ * This looks up the configuration name which stays in position "position" in the list of
+ * configuration names (which is saved in the "SAVED_CONFIGURATIONS" shared preferences file).
+ * Then uses this name to retrieve the relevant configuration from this file.
+ *
+ * @param position position of the configuration in the array of configuration names
+ * @return the configuration at position "position" or null if no such configuration was found
+ */
+ public BeaconConfiguration getConfigurationAtPosition(int position) {
+ ArrayList spConfigNames = getConfigurationNamesList();
+ String configName = spConfigNames.get(position);
+ return getConfiguration(configName);
+ }
+
+ public void deleteConfigurationWithName(String name) {
+ ArrayList spConfigNames = getConfigurationNamesList();
+ spConfigNames.remove(name);
+ SharedPreferences.Editor spEditor = activity.getSharedPreferences(
+ Constants.SAVED_CONFIGURATIONS, Context.MODE_PRIVATE).edit();
+ spEditor.remove(name);
+ spEditor.remove(Constants.CONFIG_NAMES);
+ spEditor.putString(Constants.CONFIG_NAMES, gson.toJson(spConfigNames));
+ spEditor.commit();
+ }
+
+ /**
+ * @return the list of all names of configurations saved to the "SAVED_CONFIGURATIONS" shared
+ * preferences file
+ */
+ public ArrayList getConfigurationNamesList() {
+ SharedPreferences sp = activity.getSharedPreferences(Constants.SAVED_CONFIGURATIONS,
+ Context.MODE_PRIVATE);
+ String serializedConfigNames = sp.getString(Constants.CONFIG_NAMES, null);
+ final ArrayList configNames
+ = gson.fromJson(serializedConfigNames, ArrayList.class);
+ return configNames;
+ }
+
+ public int getNumberOfConfigurations() {
+ SharedPreferences sp = activity.getSharedPreferences(
+ Constants.SAVED_CONFIGURATIONS, Context.MODE_PRIVATE);
+ String serializedConfigNames = sp.getString(Constants.CONFIG_NAMES, null);
+ ArrayList spConfigNames
+ = gson.fromJson(serializedConfigNames, ArrayList.class);
+ return spConfigNames.size();
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/ScanningActivity.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/ScanningActivity.java
new file mode 100644
index 0000000..5eff4fb
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/ScanningActivity.java
@@ -0,0 +1,274 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig;
+
+import android.Manifest;
+import android.app.Activity;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.Toolbar;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import com.github.google.beaconfig.utils.UiUtils;
+
+/**
+ * This is the main activity in Beaconfig. It asks for location permissions, turns on bluetooth and
+ * initialises the BLE Scanner. Then it scans for nearby beacons and displays the results in a list.
+ *
+ * Each entry in the list represents a unique beacon with information from several scan results
+ * whose information is saved in a BeaconScanData object.
+ *
+ * On click of any of the list entries, a new activity starts - BeaconConfigActivity. It connects
+ * to the beacon and allows per slot configuration of the beacon.
+ */
+public class ScanningActivity extends AppCompatActivity {
+ private static final int PERMISSION_COARSE_LOCATION = 2;
+
+ private static final String TAG = ScanningActivity.class.getSimpleName();
+
+ private BeaconListAdapter beaconsListAdapter;
+ private BluetoothAdapter btAdapter;
+ private BeaconScanner scanner;
+
+ private SwipeRefreshLayout swipeRefreshLayout;
+
+ private ExecutorService executor;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ new SavedConfigurationsManager(this).initialiseConfigurationSaving();
+ executor = Executors.newSingleThreadExecutor();
+
+ beaconsListAdapter
+ = new BeaconListAdapter(new ArrayList(), getApplication());
+ RecyclerView beaconsRecyclerView = (RecyclerView) findViewById(R.id.rv);
+ beaconsRecyclerView.setAdapter(beaconsListAdapter);
+ beaconsRecyclerView.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
+
+ final FloatingActionButton refresh = (FloatingActionButton) findViewById(R.id.fab);
+ refresh.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ scan();
+ }
+ });
+
+ setUpThrobber();
+ getRequiredPermissions();
+
+ beaconsRecyclerView.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent motionEvent) {
+ return swipeRefreshLayout.isRefreshing();
+ }
+ });
+
+ beaconsRecyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
+ @Override
+ public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ Log.d(TAG, "On interception touch listener " + swipeRefreshLayout.isRefreshing());
+ return swipeRefreshLayout.isRefreshing();
+ }
+
+ @Override
+ public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+
+ }
+
+ @Override
+ public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+
+ }
+ });
+
+ }
+
+ /**
+ * Attempts to scan for beacons. First checks whether bluetooth is turned on and deals with
+ * the case when it is not. During this, the screen is disabled and the swipeRefreshLayout is
+ * refreshing.
+ */
+ private void scan() {
+ Log.d(TAG, "Scanning...");
+ if (btAdapter == null || !btAdapter.isEnabled()) {
+ requestBluetoothOn();
+ } else {
+ swipeRefreshLayout.post(new Runnable() {
+ @Override
+ public void run() {
+ swipeRefreshLayout.setRefreshing(true);
+ }
+ });
+ findViewById(R.id.grey_out_slot).setVisibility(View.VISIBLE);
+ UiUtils.showToast(this, "Scanning...");
+
+ executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ scanner.scan();
+ }
+ });
+ }
+ }
+
+ /**
+ * Callback for when the scan has finished. This enables the screen again and informs the
+ * RecyclerViewAdapter that new data is available to be displayed.
+ *
+ * @param scanDataList the information gathered about each beacon over the whole time of the
+ * scan.
+ */
+ public void scanComplete(final List scanDataList) {
+ Log.d(TAG, "Scanning complete.");
+ Collections.sort(scanDataList, new Comparator() {
+ @Override
+ public int compare(BeaconScanData b1, BeaconScanData b2) {
+ return (b2.rssi - b1.rssi);
+ }
+ });
+
+
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ beaconsListAdapter.setData(scanDataList);
+ swipeRefreshLayout.post(new Runnable() {
+ @Override
+ public void run() {
+ swipeRefreshLayout.setRefreshing(false);
+ findViewById(R.id.grey_out_slot).setVisibility(View.GONE);
+ String message = scanDataList.size() + " results were found";
+ UiUtils.showToast(ScanningActivity.this,message);
+ }
+ });
+ }
+ });
+ }
+
+ private void setupScanner() {
+ Log.d(TAG, "Setting up scanner...");
+ BluetoothManager manager = (BluetoothManager) getApplicationContext()
+ .getSystemService(Context.BLUETOOTH_SERVICE);
+ btAdapter = manager.getAdapter();
+
+ requestBluetoothOn();
+ }
+
+ private void requestBluetoothOn() {
+ if (btAdapter == null || !btAdapter.isEnabled()) {
+ Log.d(TAG, "Bluetooth not enabled, requesting permission.");
+ Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
+ this.startActivityForResult(enableBtIntent, Constants.REQUEST_ENABLE_BLUETOOTH);
+ } else if (scanner == null) {
+ scanner = new BeaconScanner(this, btAdapter);
+ scan();
+ }
+ }
+
+ private void getRequiredPermissions() {
+ Log.d(TAG, "Getting Permissions...");
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.ACCESS_COARSE_LOCATION},
+ PERMISSION_COARSE_LOCATION);
+ } else {
+ setupScanner();
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (requestCode == Constants.REQUEST_ENABLE_BLUETOOTH) {
+ if (resultCode == Activity.RESULT_OK) {
+ Log.d(TAG, "Bluetooth enable permission granted.");
+ scanner = new BeaconScanner(this, btAdapter);
+ scan();
+ } else {
+ Log.d(TAG, "Bluetooth enable permission denied. Closing...");
+ showFinishingAlertDialog("Bluetooth is required",
+ "App will close since the permission was denied");
+ }
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int code, String permissions[], int[] grantResults) {
+ switch (code) {
+ case PERMISSION_COARSE_LOCATION:
+
+ if (grantResults.length > 0
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ setupScanner();
+ Log.d(TAG, "PERMISSION_REQUEST_COARSE_LOCATION granted");
+ } else {
+ showFinishingAlertDialog("Coarse location access is required",
+ "App will close since the permission was denied");
+ }
+ }
+ }
+
+ private void showFinishingAlertDialog(String title, String message) {
+ new AlertDialog.Builder(this).setTitle(title).setMessage(message)
+ .setPositiveButton("OK", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ finish();
+ }
+ }).show();
+ }
+
+ private void setUpThrobber() {
+ swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.throbber);
+ swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
+ @Override
+ public void onRefresh() {
+ scan();
+ }
+ });
+ swipeRefreshLayout.setColorSchemeResources(android.R.color.holo_blue_bright,
+ android.R.color.holo_green_light,
+ android.R.color.holo_orange_light,
+ android.R.color.holo_red_light);
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/SlotsAdapter.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/SlotsAdapter.java
new file mode 100644
index 0000000..d676393
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/SlotsAdapter.java
@@ -0,0 +1,58 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig;
+
+import android.os.Bundle;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentStatePagerAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import layout.BeaconTabFragment;
+import layout.SlotFragment;
+
+/**
+ * The SlotsAdapter handles transforming slot data to corresponding fragments which will then be
+ * displayed as tabs in the configuration page of BeaconConfig.
+ */
+public class SlotsAdapter extends FragmentStatePagerAdapter {
+ private List slotFragments;
+
+ public SlotsAdapter(FragmentManager fm) {
+ super(fm);
+ slotFragments = new ArrayList<>();
+ }
+
+ public void createNewFragment(final Bundle bundle) {
+ SlotFragment newSlotFragment = SlotFragment.newInstance(bundle);
+ slotFragments.add(newSlotFragment);
+ }
+
+ public void addFragment(BeaconTabFragment fragment) {
+ slotFragments.add(fragment);
+ }
+
+ @Override
+ public BeaconTabFragment getItem(int position) {
+ return slotFragments.get(position);
+ }
+
+ @Override
+ public int getCount() {
+ return slotFragments.size();
+ }
+
+}
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/ChangePasswordDialog.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/ChangePasswordDialog.java
new file mode 100644
index 0000000..e1f7c60
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/ChangePasswordDialog.java
@@ -0,0 +1,224 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig.dialogs;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import com.github.google.beaconfig.R;
+
+/**
+ * Dialog for when a lock code change is requested
+ */
+public class ChangePasswordDialog {
+ public static void show(final Activity activity, final PasswordChangeListener l) {
+ final AlertDialog passwordChangeDialog = new AlertDialog.Builder(activity).show();
+
+ // This is needed because there are some flags being set which prevent the keyboard from
+ // popping up when an EditText is clicked
+ passwordChangeDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+
+ passwordChangeDialog.setContentView(R.layout.dialog_change_lock_code);
+ passwordChangeDialog.setCanceledOnTouchOutside(false);
+
+ final EditText newPasswordView
+ = (EditText) passwordChangeDialog.findViewById(R.id.enter_new_lock_code);
+ final TextView newPasswordLengthTracker
+ = (TextView) passwordChangeDialog.findViewById(R.id.new_code_tracker);
+
+ newPasswordView.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ newPasswordLengthTracker.setText("(" + newPasswordView.getText().length() + "/32)");
+ }
+ });
+
+ newPasswordView.setOnEditorActionListener(new EditText.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ if (newPasswordView.getText().toString().length() < 32) {
+ newPasswordView.setError
+ ("Lock code too short!");
+ return true;
+ } else {
+ return false;
+ }
+ }
+ return false;
+ }
+ });
+
+ newPasswordView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (!hasFocus) {
+ String input = newPasswordView.getText().toString();
+ if (input.length() < 32) {
+ newPasswordView.setError("Lock code too short!");
+ }
+ }
+ }
+ });
+
+
+ final EditText repeatPasswordView
+ = (EditText) passwordChangeDialog.findViewById(R.id.repeat_new_lock_code);
+ final TextView repeatLengthTracker
+ = (TextView) passwordChangeDialog.findViewById(R.id.repeat_code_tracker);
+
+ repeatPasswordView.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ repeatLengthTracker.setText("(" + repeatPasswordView.getText().length() + "/32)");
+ }
+ });
+
+ repeatPasswordView.setOnEditorActionListener(new EditText.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ final String input = repeatPasswordView.getText().toString();
+
+ if (newPasswordView.getText().toString().length() < 32) {
+ newPasswordView.setError("Lock code too short!");
+ }
+
+ if (input.length() < 32) {
+ repeatPasswordView.setError("Lock code too short!");
+ } else if (!input.equals(newPasswordView.getText().toString())) {
+ repeatPasswordView.setError("Lock codes do not match.");
+ } else if (input.equals(newPasswordView.getText().toString())) {
+ ConfirmationDialog.confirm("Change Lock Code", "Are you sure you want "
+ + "to change the lock code of this beacon? \n \n You will not be "
+ + "able to restore the previous lock code.", "YES", "NO", activity,
+ new ConfirmationDialog.ConfirmationListener() {
+ @Override
+ public void confirm() {
+ l.passwordChangeRequest(input);
+ passwordChangeDialog.dismiss();
+ }
+
+ @Override
+ public void cancel() {
+ }
+ });
+ return false;
+ }
+ }
+ return false;
+ }
+ });
+
+ repeatPasswordView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (!hasFocus) {
+ String input = repeatPasswordView.getText().toString();
+ if (input.length() < 32) {
+ repeatPasswordView.setError("Lock code too short!");
+ } else if (!input.equals(newPasswordView.getText().toString())) {
+ repeatPasswordView.setError("Lock codes do not match.");
+ }
+ }
+ }
+ });
+
+
+
+// Override the button so we can prevent closing the dialog if the input is bogus.
+ passwordChangeDialog.findViewById(R.id.confirm_button).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final String newPassword = newPasswordView.getText().toString();
+ String repeatNewPassword = repeatPasswordView.getText().toString();
+
+ if (newPassword.isEmpty()) {
+ newPasswordView.setError("Lock code too short!");
+ }
+
+ if (repeatNewPassword.isEmpty()) {
+ repeatPasswordView.setError("Lock code too short!");
+ } else if (newPassword.equals(repeatNewPassword)) {
+ ConfirmationDialog.confirm("Change Lock Code", "Are you sure you want "
+ + "to change the lock code of this beacon? \n \n You will not be "
+ + "able to restore the previous lock code.", "OK", "CANCEL",
+ activity, new ConfirmationDialog.ConfirmationListener() {
+ @Override
+ public void confirm() {
+ l.passwordChangeRequest(newPassword);
+ passwordChangeDialog.getWindow().setSoftInputMode(
+ WindowManager.LayoutParams.
+ SOFT_INPUT_STATE_ALWAYS_HIDDEN);
+ passwordChangeDialog.dismiss();
+ }
+
+ @Override
+ public void cancel() {
+ }
+ });
+ } else if (!newPassword.equals(repeatNewPassword)) {
+ repeatPasswordView.setError("Lock codes do not match.");
+ }
+ }
+ });
+
+ passwordChangeDialog.findViewById(R.id.cancel_change_password).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ passwordChangeDialog.dismiss();
+ }
+ });
+ }
+
+ /**
+ * Used for communication between the activity calling ChangePasswordListener
+ * and ChangePasswordListener.
+ */
+ public interface PasswordChangeListener {
+ void passwordChangeRequest(String newPassword);
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/ConfirmationDialog.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/ConfirmationDialog.java
new file mode 100644
index 0000000..05440ee
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/ConfirmationDialog.java
@@ -0,0 +1,65 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig.dialogs;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+
+/**
+ * This dialog is displayed whenever there is a risky request from the user. It asks for
+ * confirmation before executing the request.
+ */
+public class ConfirmationDialog {
+ public static void confirm(final String title, final String message, final String confirmText,
+ final String cancelText, final Activity ctx,
+ final ConfirmationListener listener) {
+ ctx.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final AlertDialog confirmDialog = new AlertDialog.Builder(ctx)
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(confirmText, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ listener.confirm();
+ dialog.dismiss();
+ }
+ })
+ .setNegativeButton(cancelText, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ listener.cancel();
+ dialog.dismiss();
+ }
+ })
+ .show();
+
+ confirmDialog.setCanceledOnTouchOutside(false);
+
+ }
+ });
+ }
+
+ /**
+ * Used for communication between the activity calling ConfirmationDialog
+ * and ConfirmationDialog.
+ */
+ public interface ConfirmationListener {
+ void confirm();
+ void cancel();
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/SaveConfigurationDialog.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/SaveConfigurationDialog.java
new file mode 100644
index 0000000..c6a4d0a
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/SaveConfigurationDialog.java
@@ -0,0 +1,78 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig.dialogs;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.EditText;
+
+import com.github.google.beaconfig.R;
+import com.github.google.beaconfig.SavedConfigurationsManager;
+
+/**
+ * This dialog pops up when the user wants to save the current configuration of the beacon. It asks
+ * the user for a name which will uniquely identify this configuration. It does not allow the user
+ * to choose a name which laready exists.
+ */
+public class SaveConfigurationDialog {
+
+ public static void show(final Activity ctx, final SaveConfigListener saveConfigListener) {
+ final AlertDialog saveConfigDialog = new AlertDialog.Builder(ctx).show();
+ saveConfigDialog.setContentView(R.layout.dialog_save_config);
+ saveConfigDialog.setCanceledOnTouchOutside(false);
+
+ // This is needed because there are some flags being set which prevent the keyboard from
+ // popping up when an EditText is clicked
+ saveConfigDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+
+ final EditText configNameView
+ = (EditText) saveConfigDialog.findViewById(R.id.configuration_name);
+
+ saveConfigDialog.findViewById(R.id.confirm_button).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String configName = configNameView.getText().toString();
+ if (configName == null || configName.isEmpty()) {
+ configNameView.setError("Please enter a name");
+ } else if (new SavedConfigurationsManager(ctx)
+ .getConfigurationNamesList().contains(configName)) {
+ configNameView.setError("This configuration is already in use");
+ } else {
+ saveConfigListener.configNameChosen(configName);
+ saveConfigDialog.dismiss();
+ }
+ }
+ });
+
+ saveConfigDialog.findViewById(R.id.cancel_save_config).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ saveConfigDialog.dismiss();
+ }
+ });
+ }
+
+ /**
+ * Listener interface to be called when a name has been chosen for this configuration
+ */
+ public interface SaveConfigListener {
+ void configNameChosen(String configName);
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/UidSlotDataChangeDialog.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/UidSlotDataChangeDialog.java
new file mode 100644
index 0000000..6270ca4
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/UidSlotDataChangeDialog.java
@@ -0,0 +1,126 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig.dialogs;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import com.github.google.beaconfig.R;
+
+/**
+ * Dialog which pops up when the user wants to change either the namespace or the instance of the
+ * Uid slot. It verifies the input and doesn't allow invalid information to be sent to the beacon.
+ */
+public class UidSlotDataChangeDialog {
+ public static void show(String oldNamespace, String oldInstance,
+ final Context ctx, final UidChangeListener uidChangeListener) {
+ final AlertDialog editUidDialog = new AlertDialog.Builder(ctx).show();
+ editUidDialog.setContentView(R.layout.dialog_change_uid);
+ editUidDialog.setCanceledOnTouchOutside(false);
+
+ // This is needed because there are some flags being set which prevent the keyboard from
+ // popping up when an EditText is clicked
+ editUidDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+
+ final EditText editNamespaceView
+ = (EditText) editUidDialog.findViewById(R.id.edit_namespace);
+ final TextView namespaceTracker
+ = (TextView) editUidDialog.findViewById(R.id.namespace_tracker);
+ editNamespaceView.setText(oldNamespace);
+ editNamespaceView.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ namespaceTracker.setText("(" + editNamespaceView.getText().length() + "/20)");
+ }
+ });
+
+ final EditText editInstanceView
+ = (EditText) editUidDialog.findViewById(R.id.edit_instance);
+ editInstanceView.setText(oldInstance);
+ final TextView instanceTracker
+ = (TextView) editUidDialog.findViewById(R.id.instance_tracker);
+
+ editInstanceView.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ instanceTracker.setText("(" + editInstanceView.getText().length() + "/12)");
+ }
+ });
+
+ editUidDialog.findViewById(R.id.confirm_button).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final String newNamespace = editNamespaceView.getText().toString();
+ final String newInstance = editInstanceView.getText().toString();
+
+ if (newNamespace.length() < 20) {
+ editNamespaceView.setError("Must be exactly 20 hex characters");
+ }
+
+ if (newInstance.length() < 12) {
+ editInstanceView.setError("Must be exactly 12 hex characters");
+ }
+
+ if (newNamespace.length() == 20 && newInstance.length() == 12) {
+ uidChangeListener.setNewUid(newNamespace, newInstance);
+ editUidDialog.dismiss();
+ }
+ }
+ });
+
+ editUidDialog.findViewById(R.id.cancel_change_uid).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ editUidDialog.dismiss();
+ }
+ });
+ }
+
+ /**
+ * Listener interface to be called when password has been types in.
+ */
+ public interface UidChangeListener {
+ void setNewUid(String namespace, String instance);
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/UnlockDialog.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/UnlockDialog.java
new file mode 100644
index 0000000..3e1629f
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/UnlockDialog.java
@@ -0,0 +1,150 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig.dialogs;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import com.github.google.beaconfig.R;
+import com.github.google.beaconfig.utils.Utils;
+
+/**
+ * This Dialog pops up when a password is required by the user to unlock the beacon manually.
+ */
+public class UnlockDialog {
+ public static void show(final Activity ctx, final UnlockListener unlockListener) {
+ ctx.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ final AlertDialog unlockDialog = new AlertDialog.Builder(ctx).show();
+ unlockDialog.setContentView(R.layout.dialog_unlock);
+ unlockDialog.setCanceledOnTouchOutside(false);
+
+ // This is needed because there are some flags being set which prevent the keyboard
+ // from popping up when an EditText is clicked
+ unlockDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+
+ final EditText unlockCodeView
+ = (EditText) unlockDialog.findViewById(R.id.enter_lock_code);
+ final TextView newPasswordLengthTracker
+ = (TextView) unlockDialog.findViewById(R.id.lock_code_tracker);
+
+ unlockCodeView.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence charSequence,
+ int i, int i1, int i2) {
+
+ }
+
+ @Override
+ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
+
+ }
+
+ @Override
+ public void afterTextChanged(Editable editable) {
+ newPasswordLengthTracker.setText("("
+ + unlockCodeView.getText().length() + "/32)");
+ }
+ });
+
+ unlockCodeView.setOnEditorActionListener(new EditText.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ if (unlockCodeView.getText().toString().length() < 32) {
+ unlockCodeView.setError("Lock code must be 32 hex characters.");
+ return true;
+ } else {
+ return false;
+ }
+ }
+ return false;
+ }
+ });
+
+ unlockCodeView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (!hasFocus) {
+ String input = unlockCodeView.getText().toString();
+ if (input.length() < 32 && input.length() > 0) {
+ unlockCodeView.setError("Lock code too short!");
+ }
+ }
+ }
+ });
+
+ unlockDialog.findViewById(R.id.confirm_button).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final String newPassword = unlockCodeView.getText().toString();
+
+ if (newPassword.length() < 32) {
+ unlockCodeView.setError(
+ "Please enter a 32 character lock code");
+ return;
+ }
+ unlockListener.unlock(Utils.toByteArray
+ (unlockCodeView.getText().toString()));
+ unlockDialog.dismiss();
+ }
+ });
+
+ unlockDialog.findViewById(R.id.exit).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ unlockListener.unlockingDismissed();
+ unlockDialog.dismiss();
+ }
+ });
+
+ unlockDialog.setOnKeyListener(new Dialog.OnKeyListener() {
+
+ @Override
+ public boolean onKey(DialogInterface arg0, int keyCode,
+ KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ unlockListener.unlockingDismissed();
+ unlockDialog.dismiss();
+ }
+ return true;
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Listener interface to be called when password has been types in.
+ */
+ public interface UnlockListener {
+ void unlockingDismissed();
+ void unlock(byte[] unlockCode);
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/UrlChangeDialog.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/UrlChangeDialog.java
new file mode 100644
index 0000000..66733a0
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/dialogs/UrlChangeDialog.java
@@ -0,0 +1,77 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig.dialogs;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.Spinner;
+
+import com.github.google.beaconfig.R;
+
+/**
+ * Dialog used to change url in a url frame. It detects when the input is wrongly formulated and
+ * does not allow wrong urls to be sent to the beacon.
+ */
+public class UrlChangeDialog {
+ public static void show(final Context ctx, final UrlChangeListener urlChangeListener) {
+ final AlertDialog editUrlDialog = new AlertDialog.Builder(ctx).show();
+ editUrlDialog.setContentView(R.layout.dialog_change_url);
+ editUrlDialog.setCanceledOnTouchOutside(false);
+
+ // This is needed because there are some flags being set which prevent the keyboard from
+ // popping up when an EditText is clicked
+ editUrlDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
+
+ final EditText editUrlView
+ = (EditText) editUrlDialog.findViewById(R.id.edit_url);
+
+ ArrayAdapter spinnerAdapter = ArrayAdapter.createFromResource(ctx,
+ R.array.url_schemes, R.layout.spinner_item);
+ spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ final Spinner urlSpinner = (Spinner) editUrlDialog.findViewById(R.id.url_spinner);
+ urlSpinner.setAdapter(spinnerAdapter);
+
+ editUrlDialog.findViewById(R.id.confirm_button).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ String newUrl = urlSpinner.getSelectedItem()
+ + editUrlView.getText().toString();
+ urlChangeListener.setNewUrl(newUrl);
+ editUrlDialog.dismiss();
+ }
+ });
+
+ editUrlDialog.findViewById(R.id.cancel_change_url).setOnClickListener(
+ new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ editUrlDialog.dismiss();
+ }
+ });
+ }
+
+ /**
+ * Listener interface to be called when password has been types in.
+ */
+ public interface UrlChangeListener {
+ void setNewUrl(String newUrl);
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattClient.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattClient.java
new file mode 100644
index 0000000..e53c938
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattClient.java
@@ -0,0 +1,534 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig.gatt;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.util.Log;
+
+import com.github.google.beaconfig.Constants;
+import com.github.google.beaconfig.utils.Utils;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Gatt Client to take care of reading and writing operations using the Gatt service. It makes
+ * calls to the GATT service synchronous.
+ */
+public class GattClient {
+ private static final String TAG = GattClient.class.getSimpleName();
+
+ // When asked to read, write, or discover services after connecting, we'll sleep the current
+ // thread for this long. This is HIGHLY non-scientific. I have a lose theory that the BT stack
+ // on some devices doesn't like being thrashed, and if the beacon responds very quickly to
+ // requests we can get the phone into a bad state. This seems to help, but it's really just
+ // cargo-cult programming.
+ private static final int INTER_OP_SLEEP_MILLIS = 20;
+
+ // The amount of time we allow reads/writes to return. It looks like Android L/M will wait
+ // for up to 30 secs for completion, but my experience is that a disconnection happens if
+ // the beacon is blocking the call and won't return.
+ private static final int READ_WRITE_TIMEOUT_SECS = 20;
+ private static final int MAX_EID_SLOT_READ_ATTEMPTS = 5;
+ private static final int VALID_EID_SLOT_DATA_LENGTH = 14;
+ private BluetoothGattCharacteristic cBroadcastCapabilities;
+ private BluetoothGattCharacteristic cActiveSlot;
+ private BluetoothGattCharacteristic cAdvertisingInterval;
+ private BluetoothGattCharacteristic cRadioTxPower;
+ private BluetoothGattCharacteristic cAdvertisedTxPower;
+ private BluetoothGattCharacteristic cLockState;
+ private BluetoothGattCharacteristic cUnlock;
+ private BluetoothGattCharacteristic cPublicEcdhKey;
+ private BluetoothGattCharacteristic cEidIdentityKey;
+ private BluetoothGattCharacteristic cAdvSlotData;
+ private BluetoothGattCharacteristic cFactoryReset;
+ private BluetoothGattCharacteristic cRemainConnectable;
+
+ // Callbacks in BluetoothGattCallback are delivered on a thread pool that the system owns.
+ // Don't try to use a newSingleThreadExecutor service here -- trying to read the Future<>
+ // from the executor will likely be blocked on the callback trying to use the same thread.
+ private ExecutorService executor = Executors.newFixedThreadPool(3);
+ private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
+
+ @Override
+ public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+ if (newState == BluetoothProfile.STATE_CONNECTED) {
+ listener.onGattConnected();
+ } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+ gatt.close();
+ listener.onGattDisconnected();
+ }
+ }
+
+ @Override
+ public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ handleServicesDiscovered();
+ listener.onGattServicesDiscovered();
+ }
+ });
+ }
+
+ @Override
+ public void onCharacteristicRead(final BluetoothGatt gatt,
+ final BluetoothGattCharacteristic characteristic,
+ final int status) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ byte[] data = characteristic.getValue();
+ Log.d(TAG, "onCharacteristicRead for "
+ + GattConstants.getReadableName(characteristic)
+ + ", data: " + Utils.toHexString(data));
+
+ resultQueue.add(new GattResult(status, data));
+ }
+ });
+ }
+
+ @Override
+ public void onCharacteristicWrite(final BluetoothGatt gatt,
+ final BluetoothGattCharacteristic characteristic,
+ final int status) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ byte[] data = characteristic.getValue();
+ Log.d(TAG, "onCharacteristicWrite for "
+ + GattConstants.getReadableName(characteristic)
+ + ", data: " + Utils.toHexString(data));
+
+ resultQueue.add(new GattResult(status, data));
+ }
+ });
+ }
+ };
+
+ private final Context context;
+ private final String deviceAddress;
+ private final GattListener listener;
+ private BluetoothAdapter bluetoothAdapter;
+ private BluetoothGatt bluetoothGatt;
+ private boolean isEddystoneGattServicePresent = false;
+ private List missingCharacteristics = new ArrayList<>();
+
+ // Basic blocking queue of size 1. Every operation through this client is synchronous. It's the
+ // caller's job to chain together operations based on the success of each function implemented
+ // by this client.
+ private BlockingQueue resultQueue = new ArrayBlockingQueue<>(1);
+
+ /**
+ * The activity handles the connection and disconnection events.
+ */
+ public interface GattListener {
+ void onGattConnected();
+ void onGattServicesDiscovered();
+ void onGattDisconnected();
+ }
+
+ private class GattResult {
+ public final int status;
+ public final byte[] data;
+ public GattResult(int status, byte[] data) {
+ this.status = status;
+ this.data = data;
+ }
+ }
+
+ public GattClient(Context context, String deviceAddress, GattListener listener) {
+ this.context = context;
+ this.deviceAddress = deviceAddress;
+ this.listener = listener;
+ BluetoothManager bluetoothManager = (BluetoothManager) context
+ .getSystemService(Context.BLUETOOTH_SERVICE);
+ bluetoothAdapter = bluetoothManager.getAdapter();
+ }
+
+ public boolean connect() {
+ if (bluetoothAdapter == null) {
+ Log.e(TAG, "Unable to obtain a BluetoothAdapter");
+ return false;
+ }
+
+ BluetoothDevice bluetoothDevice = bluetoothAdapter.getRemoteDevice(deviceAddress);
+ if (bluetoothDevice == null) {
+ Log.e(TAG, "Device " + deviceAddress + " not found, unable to connect");
+ return false;
+ }
+
+ bluetoothGatt = bluetoothDevice.connectGatt(context, false, gattCallback);
+ Log.d(TAG, "Trying to create a new connection");
+ return true;
+ }
+
+ public void discoverServices() {
+ if (bluetoothGatt != null) {
+ Utils.sleep(INTER_OP_SLEEP_MILLIS);
+ bluetoothGatt.discoverServices();
+ }
+ }
+
+ private void handleServicesDiscovered() {
+ for (BluetoothGattService service : bluetoothGatt.getServices()) {
+ Log.d(TAG, "discovered service " + service.getUuid());
+
+ if (service.getUuid().equals(Constants.EDDYSTONE_CONFIGURATION_UUID.getUuid())) {
+ isEddystoneGattServicePresent = true;
+ Log.d(TAG, "this is the eddystone service");
+
+ for (BluetoothGattCharacteristic c : service.getCharacteristics()) {
+ String uuid = c.getUuid().toString();
+ if (uuid.equalsIgnoreCase(GattConstants.CHAR_BROADCAST_CAPABILITIES)) {
+ cBroadcastCapabilities = c;
+ } else if (uuid.equalsIgnoreCase(GattConstants.CHAR_ACTIVE_SLOT)) {
+ cActiveSlot = c;
+ } else if (uuid.equalsIgnoreCase(GattConstants.CHAR_ADVERTISING_INTERVAL)) {
+ cAdvertisingInterval = c;
+ } else if (uuid.equalsIgnoreCase(GattConstants.CHAR_RADIO_TX_POWER)) {
+ cRadioTxPower = c;
+ } else if (uuid.equalsIgnoreCase(GattConstants.CHAR_ADVERTISED_TX_POWER)) {
+ cAdvertisedTxPower = c;
+ } else if (uuid.equalsIgnoreCase(GattConstants.CHAR_LOCK_STATE)) {
+ cLockState = c;
+ } else if (uuid.equalsIgnoreCase(GattConstants.CHAR_UNLOCK)) {
+ cUnlock = c;
+ } else if (uuid.equalsIgnoreCase(GattConstants.CHAR_PUBLIC_ECDH_KEY)) {
+ cPublicEcdhKey = c;
+ } else if (uuid.equalsIgnoreCase(GattConstants.CHAR_EID_IDENTITY_KEY)) {
+ cEidIdentityKey = c;
+ } else if (uuid.equalsIgnoreCase(GattConstants.CHAR_ADV_SLOT_DATA)) {
+ cAdvSlotData = c;
+ } else if (uuid.equalsIgnoreCase(GattConstants.CHAR_FACTORY_RESET)) {
+ cFactoryReset = c;
+ } else if (uuid.equalsIgnoreCase(GattConstants.CHAR_REMAIN_CONNECTABLE)) {
+ cRemainConnectable = c;
+ }
+ }
+
+ Log.d(TAG, cUnlock.toString());
+ Log.d(TAG, "Checking characteristics");
+
+ for (UUID uuid : GattConstants.CHAR_UUIDS) {
+ if (service.getCharacteristic(uuid) == null) {
+ missingCharacteristics.add(uuid);
+ Log.d(TAG, GattConstants.getReadableName(uuid) + " missing");
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ public void disconnect() {
+ if (bluetoothGatt == null) {
+ return;
+ }
+ bluetoothGatt.close();
+ bluetoothGatt.disconnect();
+ Log.d(TAG, "Just disconnected.");
+ bluetoothGatt = null;
+ }
+
+ public boolean isEddystoneGattServicePresent() {
+ return isEddystoneGattServicePresent;
+ }
+
+ public List getMissingCharacteristics() {
+ return missingCharacteristics;
+ }
+
+ private class GattCallable implements Callable {
+ @Override
+ public GattResult call() throws Exception {
+ try {
+ return resultQueue.take();
+ } catch (InterruptedException e) {
+ Log.d(TAG, "InterruptedException, returning error result");
+ return new GattResult(-1, null);
+ }
+ }
+ }
+
+ // Read and writes of raw data for each characteristic.
+ public byte[] readBroadcastCapabilities() throws GattClientException, GattOperationException {
+ return read(cBroadcastCapabilities);
+ }
+
+ public byte[] readActiveSlot() throws GattClientException, GattOperationException {
+ return read(cActiveSlot);
+ }
+
+ public byte[] writeActiveSlot(int slot) throws GattClientException, GattOperationException {
+ byte[] data = new byte[] {(byte) slot};
+ return write(cActiveSlot, data);
+ }
+
+ public byte[] readAdvertisingInterval() throws GattClientException, GattOperationException {
+ return read(cAdvertisingInterval);
+ }
+
+ public byte[] writeAdvertisingInterval(int millis)
+ throws GattClientException, GattOperationException {
+ byte[] data = Utils.toTwoByteArray(millis);
+ return write(cAdvertisingInterval, data);
+ }
+
+ public byte[] readRadioTxPower() throws GattClientException, GattOperationException {
+ return read(cRadioTxPower);
+ }
+
+ public byte[] writeRadioTxPower(int dbm) throws GattClientException, GattOperationException {
+ byte[] data = new byte[] {(byte) dbm};
+ return write(cRadioTxPower, data);
+ }
+
+ public byte[] readAdvertisedTxPower() throws GattClientException, GattOperationException {
+ return read(cAdvertisedTxPower);
+ }
+
+ public byte[] writeAdvertisedTxPower(int dbm)
+ throws GattClientException, GattOperationException {
+ byte[] data = new byte[] {(byte) dbm};
+ return write(cAdvertisedTxPower, data);
+ }
+
+ public byte[] readLockState() throws GattClientException, GattOperationException {
+ return read(cLockState);
+ }
+
+ public byte[] writeLockState(byte[] data) throws GattClientException, GattOperationException {
+ return write(cLockState, data);
+ }
+
+ public byte[] readUnlock() throws GattClientException, GattOperationException {
+ return read(cUnlock);
+ }
+
+ public byte[] writeUnlock(byte[] data) throws GattClientException, GattOperationException {
+ return write(cUnlock, data);
+ }
+
+ public byte[] readPublicEcdhKey() throws GattClientException, GattOperationException {
+ return read(cPublicEcdhKey);
+ }
+
+ public byte[] readEidIdentityKey() throws GattClientException, GattOperationException {
+ return read(cEidIdentityKey);
+ }
+
+ public byte[] writeEidIdentityKey(byte[] data)
+ throws GattClientException, GattOperationException {
+ return write(cEidIdentityKey, data);
+ }
+
+ public byte[] readAdvSlotData() throws GattClientException, GattOperationException {
+ byte[] result = read(cAdvSlotData);
+ return result;
+ }
+
+ public byte[] writeAdvSlotData(byte[] data) throws GattClientException, GattOperationException {
+ return write(cAdvSlotData, data);
+ }
+
+ public byte[] writeFactoryReset(byte[] data)
+ throws GattClientException, GattOperationException {
+ return write(cFactoryReset, data);
+ }
+
+ public byte[] readRemainConnectable() throws GattClientException, GattOperationException {
+ return read(cRemainConnectable);
+ }
+
+ public byte[] writeRemainConnectable(byte[] data)
+ throws GattClientException, GattOperationException {
+ return write(cRemainConnectable, data);
+ }
+
+ private byte[] read(BluetoothGattCharacteristic characteristic)
+ throws GattClientException, GattOperationException {
+ Utils.sleep(INTER_OP_SLEEP_MILLIS);
+ Log.d(TAG, "reading " + GattConstants.getReadableName(characteristic));
+ if (bluetoothGatt != null) {
+ bluetoothGatt.readCharacteristic(characteristic);
+ return getResult(characteristic, GattOperation.OP_READ);
+ } else {
+ throw new GattClientException(characteristic.getUuid(), GattOperation.OP_READ,
+ GattClientException.ERROR_GATT_DISCONNECTED,
+ "Bluetooth Gatt disconnected unexpectedly");
+ }
+ }
+
+ private byte[] write(BluetoothGattCharacteristic characteristic, byte[] data)
+ throws GattClientException, GattOperationException {
+ Utils.sleep(INTER_OP_SLEEP_MILLIS);
+ Log.d(TAG, "writing " + GattConstants.getReadableName(characteristic)
+ + ", data: " + Utils.toHexString(data));
+ characteristic.setValue(data);
+
+ if (bluetoothGatt != null) {
+ bluetoothGatt.writeCharacteristic(characteristic);
+ return getResult(characteristic, GattOperation.OP_WRITE);
+ } else {
+ throw new GattClientException(characteristic.getUuid(), GattOperation.OP_WRITE,
+ GattClientException.ERROR_GATT_DISCONNECTED,
+ "Bluetooth Gatt disconnected unexpectedly");
+ }
+ }
+
+ private byte[] getResult(BluetoothGattCharacteristic c, int op)
+ throws GattClientException, GattOperationException {
+ Future future = executor.submit(new GattCallable());
+ try {
+ GattResult result = future.get(READ_WRITE_TIMEOUT_SECS, TimeUnit.SECONDS);
+ Log.d(TAG, "future returned status " + result.status
+ + ", data " + Utils.toHexString(result.data));
+
+ if (result.status != BluetoothGatt.GATT_SUCCESS) {
+ throw new GattOperationException(c.getUuid(), op, result.status, result.data);
+ }
+
+ return result.data;
+ } catch (InterruptedException e) {
+ Log.d(TAG, "InterruptedException in future.get, returning error result");
+ throw new GattClientException(c.getUuid(), op,
+ GattOperationException.ERROR_INTERRUPTED);
+ } catch (ExecutionException e) {
+ Log.d(TAG, "ExecutionException in future.get, returning error result");
+ throw new GattClientException(c.getUuid(), op, GattOperationException.ERROR_EXECUTION);
+ } catch (TimeoutException e) {
+ Log.d(TAG, "TimeoutException in future.get, returning error result");
+ throw new GattClientException(c.getUuid(), op, GattOperationException.ERROR_TIMEOUT);
+ }
+ }
+
+ /**
+ * Convenience methods.
+ * Simple struct representing a parsed slot data byte array for an EID slot.
+ */
+ public class EidSlotData {
+ public final byte exponent;
+ public final long clock;
+ public final byte[] eid;
+ public EidSlotData(final byte exponent, final long clock, final byte[] eid) {
+ this.exponent = exponent;
+ this.clock = clock;
+ this.eid = eid;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.UK, "exponent=%d, clock=%d, eid=%s",
+ (int) exponent, (int) clock, Utils.toHexString(eid));
+ }
+ }
+
+ /**
+ * This is a special-case of readAdvSlotData(). Some devices are slow about their cryptographic
+ * computations and can take a while to return the EID data. Instead of blocking on the read
+ * call, they either return a zeroed EID value, or an incorrect length. We deal with these cases
+ * here by checking and re-reading after a pause, giving up if necessary.
+ */
+ public EidSlotData readEidSlotData() throws GattClientException, GattOperationException {
+ int attempt = 1;
+ Log.d(TAG, "readEidSlotData attempt " + attempt);
+ byte[] data = readAdvSlotData();
+ while (data.length != VALID_EID_SLOT_DATA_LENGTH || isEidValueZeroed(data)) {
+ if (attempt == MAX_EID_SLOT_READ_ATTEMPTS) {
+ Log.d(TAG, "readEidSlotData max attempts of "
+ + MAX_EID_SLOT_READ_ATTEMPTS
+ + " reached, giving up");
+ throw new GattClientException(cAdvSlotData.getUuid(), GattClientException.OP_READ,
+ -1, "Repeated reads from slot data characteristic returned invalid data. "
+ + "Expected 14 bytes, last value read was " + Utils.toHexString(data));
+ }
+ attempt++;
+ Log.d(TAG, "readEidSlotData, data is wrong length or zeroed, "
+ + "retrying after 2 secs, attempt " + attempt);
+ Utils.sleep(2000);
+ data = readAdvSlotData();
+ }
+
+ ByteBuffer buf = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
+ byte frameType = buf.get();
+
+ if (frameType != Constants.EID_FRAME_TYPE) {
+ throw new GattClientException(cAdvSlotData.getUuid(), GattClientException.OP_READ, -1,
+ "Reading EID slot returned invalid frame type. "
+ + "Expected 0x30, got " + frameType);
+ }
+ byte exponent = buf.get();
+
+ // We want an unsigned int, so get the long and mask it.
+ long clock = (long) buf.getInt() & 0xffffffffL;
+ byte[] eidValue = new byte[8];
+ buf.get(eidValue);
+ return new EidSlotData(exponent, clock, eidValue);
+ }
+
+ // Caller guarantees length is 14.
+ private boolean isEidValueZeroed(final byte[] eidSlotData) {
+ byte[] value = new byte[8];
+ System.arraycopy(eidSlotData, 6, value, 0, 8);
+ return Utils.isZeroed(value);
+ }
+
+ // Convenience methods.
+ public void performFactoryReset() throws GattClientException, GattOperationException {
+ writeFactoryReset(new byte[]{0x0B});
+ }
+
+ public boolean unlock(byte[] unlockCode) {
+ Log.d(TAG, "Starting unlocking...");
+ try {
+ byte[] readUnlockResult = readUnlock();
+ byte[] encrypted = Utils.aes128Encrypt(readUnlockResult, unlockCode);
+ if (encrypted == null) {
+ return false;
+ }
+
+ writeUnlock(encrypted);
+ byte[] lockState = readLockState();
+ return lockState != null
+ && lockState.length > 0
+ && lockState[0] == GattConstants.LOCK_STATE_UNLOCKED;
+ } catch (GattClientException | GattOperationException e) {
+ Log.e(TAG, "Failed to unlock beacon", e);
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattClientException.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattClientException.java
new file mode 100644
index 0000000..3765401
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattClientException.java
@@ -0,0 +1,79 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig.gatt;
+
+import java.util.Locale;
+import java.util.UUID;
+/**
+ * Basic wrapper for an error in the GattClient.
+ */
+public class GattClientException extends Exception {
+ // Supported operations.
+ public static final int OP_READ = 1;
+ public static final int OP_WRITE = 2;
+ // Errors beyond the canonical errors in the BluetoothGatt class.
+ public static final int ERROR_INTERRUPTED = -1;
+ public static final int ERROR_EXECUTION = -2;
+ public static final int ERROR_TIMEOUT = -3;
+ public static final int ERROR_GATT_DISCONNECTED = -4;
+ public final UUID uuid;
+ public final int op;
+ public final int error;
+ public final String customMessage;
+ /**
+ * Wraps a GATT client error that prevented the GATT operation from completing.
+ *
+ * @param uuid the UUID of the characteristic that triggered the error.
+ * @param op the operation that triggered the error, defined by one of the constants in
+ * the GattOperation class.
+ * @param error maps to one of the ERROR_XXX constants in this class. The data field will be
+ * null since the GATT operation did not complete.
+ * @param customMessage if not null, provides a custom message to return in getMessage()
+ */
+ public GattClientException(UUID uuid, int op, int error, String customMessage) {
+ this.uuid = uuid;
+ this.op = op;
+ this.error = error;
+ this.customMessage = customMessage;
+ }
+ /**
+ * Wraps a GATT client error that prevented the GATT operation from completing.
+ *
+ * @param uuid the UUID of the characteristic that triggered the error.
+ * @param op the operation that triggered the error, defined by one of the constants in
+ * the GattOperation class.
+ * @param error maps to one of the ERROR_XXX constants in this class. The data field will be
+ * null since the GATT operation did not complete.
+ */
+ public GattClientException(UUID uuid, int op, int error) {
+ this(uuid, op, error, null);
+ }
+ @Override
+ public String getMessage() {
+ String errStr = error == ERROR_INTERRUPTED
+ ? "InterruptionException" : error == ERROR_EXECUTION
+ ? "ExecutionException" : "TimeoutException";
+ String opStr = op == GattOperation.OP_READ ? "reading" : "writing";
+ String message = String.format(Locale.UK,
+ "Client error %s when %s characteristic %s",
+ errStr,
+ opStr,
+ GattConstants.getReadableName(uuid));
+ if (customMessage != null) {
+ message += "Detail: " + customMessage;
+ }
+ return message;
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattConstants.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattConstants.java
new file mode 100644
index 0000000..6166d78
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattConstants.java
@@ -0,0 +1,135 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig.gatt;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+/**
+ * Constants for the Eddystone GATT configuration service.
+ */
+public class GattConstants {
+ private GattConstants() {}
+ private static final String UNKNOWN_NAME = "Unknown characteristic";
+ private static HashMap attributes = new HashMap<>();
+ public static final String ES_CONFIGURATION_SERVICE_UUID
+ = "a3c87500-8ed3-4bdf-8a39-a01bebede295";
+ public static final String CHAR_BROADCAST_CAPABILITIES = "a3c87501-8ed3-4bdf-8a39-a01bebede295";
+ public static final String CHAR_ACTIVE_SLOT = "a3c87502-8ed3-4bdf-8a39-a01bebede295";
+ public static final String CHAR_ADVERTISING_INTERVAL = "a3c87503-8ed3-4bdf-8a39-a01bebede295";
+ public static final String CHAR_RADIO_TX_POWER = "a3c87504-8ed3-4bdf-8a39-a01bebede295";
+ public static final String CHAR_ADVERTISED_TX_POWER = "a3c87505-8ed3-4bdf-8a39-a01bebede295";
+ public static final String CHAR_LOCK_STATE = "a3c87506-8ed3-4bdf-8a39-a01bebede295";
+ public static final String CHAR_UNLOCK = "a3c87507-8ed3-4bdf-8a39-a01bebede295";
+ public static final String CHAR_PUBLIC_ECDH_KEY = "a3c87508-8ed3-4bdf-8a39-a01bebede295";
+ public static final String CHAR_EID_IDENTITY_KEY = "a3c87509-8ed3-4bdf-8a39-a01bebede295";
+ public static final String CHAR_ADV_SLOT_DATA = "a3c8750a-8ed3-4bdf-8a39-a01bebede295";
+ public static final String CHAR_FACTORY_RESET = "a3c8750b-8ed3-4bdf-8a39-a01bebede295";
+ public static final String CHAR_REMAIN_CONNECTABLE = "a3c8750c-8ed3-4bdf-8a39-a01bebede295";
+ public static final List CHAR_UUIDS = new ArrayList<>();
+ static {
+ CHAR_UUIDS.add(UUID.fromString(CHAR_BROADCAST_CAPABILITIES));
+ CHAR_UUIDS.add(UUID.fromString(CHAR_ACTIVE_SLOT));
+ CHAR_UUIDS.add(UUID.fromString(CHAR_ADVERTISING_INTERVAL));
+ CHAR_UUIDS.add(UUID.fromString(CHAR_RADIO_TX_POWER));
+ CHAR_UUIDS.add(UUID.fromString(CHAR_ADVERTISED_TX_POWER));
+ CHAR_UUIDS.add(UUID.fromString(CHAR_LOCK_STATE));
+ CHAR_UUIDS.add(UUID.fromString(CHAR_UNLOCK));
+ CHAR_UUIDS.add(UUID.fromString(CHAR_PUBLIC_ECDH_KEY));
+ CHAR_UUIDS.add(UUID.fromString(CHAR_EID_IDENTITY_KEY));
+ CHAR_UUIDS.add(UUID.fromString(CHAR_ADV_SLOT_DATA));
+ CHAR_UUIDS.add(UUID.fromString(CHAR_FACTORY_RESET));
+ CHAR_UUIDS.add(UUID.fromString(CHAR_REMAIN_CONNECTABLE));
+ }
+ static {
+ attributes.put(ES_CONFIGURATION_SERVICE_UUID, "Eddystone Configuration Service");
+ attributes.put(CHAR_BROADCAST_CAPABILITIES, "Broadcast Capabilities");
+ attributes.put(CHAR_ACTIVE_SLOT, "Active Slot");
+ attributes.put(CHAR_ADVERTISING_INTERVAL, "Advertising Interval");
+ attributes.put(CHAR_RADIO_TX_POWER, "Radio Tx Power");
+ attributes.put(CHAR_ADVERTISED_TX_POWER, "(Advanced) Advertised Tx Power");
+ attributes.put(CHAR_LOCK_STATE, "Lock State");
+ attributes.put(CHAR_UNLOCK, "Unlock");
+ attributes.put(CHAR_PUBLIC_ECDH_KEY, "Public ECDH Key");
+ attributes.put(CHAR_EID_IDENTITY_KEY, "EID Identity Key");
+ attributes.put(CHAR_ADV_SLOT_DATA, "ADV Slot Data");
+ attributes.put(CHAR_FACTORY_RESET, "(Advanced) Factory reset");
+ attributes.put(CHAR_REMAIN_CONNECTABLE, "(Advanced) Remain Connectable");
+ }
+ public static final byte CAPABILITIES_IS_VARIABLE_ADV_SUPPORTED = 0x01;
+ public static final byte CAPABILITIES_IS_VARIABLE_TX_POWER_SUPPORTED = 0x02;
+ public static final byte CAPABILITIES_IS_UID_FRAME_SUPPORTED = 0x0001;
+ public static final byte CAPABILITIES_IS_URL_FRAME_SUPPORTED = 0x0002;
+ public static final byte CAPABILITIES_IS_TLM_FRAME_SUPPORTED = 0x0004;
+ public static final byte CAPABILITIES_IS_EID_FRAME_SUPPORTED = 0x0008;
+ public static final byte LOCK_STATE_UNKNOWN = Byte.MIN_VALUE;
+ public static final byte LOCK_STATE_LOCKED = 0x00;
+ public static final byte LOCK_STATE_UNLOCKED = 0x01;
+ public static final byte LOCK_STATE_UNLOCKED_AND_AUTOMATIC_RELOCK_DISABLED = 0x02;
+ public static String lockStateAsString(byte lockState) {
+ switch (lockState) {
+ case LOCK_STATE_LOCKED:
+ return "locked";
+ case LOCK_STATE_UNLOCKED:
+ return "unlocked";
+ case LOCK_STATE_UNLOCKED_AND_AUTOMATIC_RELOCK_DISABLED:
+ return "unlocked, autolock disabled";
+ default: return "unknown lock state";
+ }
+ }
+ public static final byte REMAIN_CONNECTABLE_NOT_SUPPORTED = 0x00;
+ public static String getReadableName(String uuid) {
+ String name = attributes.get(uuid);
+ return name == null ? UNKNOWN_NAME : name;
+ }
+ public static String getReadableName(UUID uuid) {
+ return getReadableName(uuid.toString());
+ }
+ public static String getReadableName(BluetoothGattCharacteristic characteristic) {
+ return getReadableName(characteristic.getUuid());
+ }
+ public static String getStatusString(int status) {
+ switch (status) {
+ case BluetoothGatt.GATT_SUCCESS: return "GATT_SUCCESS";
+ case BluetoothGatt.GATT_READ_NOT_PERMITTED: return "READ_NOT_PERMITTED";
+ case BluetoothGatt.GATT_WRITE_NOT_PERMITTED: return "WRITE_NOT_PERMITTED";
+ case BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION:
+ return "INSUFFICIENT_AUTHENTICATION";
+ case BluetoothGatt.GATT_REQUEST_NOT_SUPPORTED: return "REQUEST_NOT_SUPPORTED";
+ case BluetoothGatt.GATT_INVALID_OFFSET: return "INVALID_OFFSET";
+ case BluetoothGatt.GATT_INVALID_ATTRIBUTE_LENGTH: return "INVALID_ATTRIBUTE_LENGTH";
+ case BluetoothGatt.GATT_INSUFFICIENT_ENCRYPTION: return "INSUFFICIENT_ENCRYPTION";
+ case BluetoothGatt.GATT_CONNECTION_CONGESTED: return "CONNECTION_CONGESTED";
+ case BluetoothGatt.GATT_FAILURE: return "GATT_FAILURE";
+ // Not first-class errors in Android, but iOS calls them out:
+ // https://developer.apple.com/library/mac/documentation/CoreBluetooth/Reference/
+ // CoreBluetooth_Constants/#//apple_ref/c/tdef/CBATTError
+ case 1: return "INVALID_HANDLE";
+ case 4: return "INVALID_PDU";
+ case 8: return "INSUFFICIENT_AUTHORIZATION";
+ case 9: return "PREPARE_QUEUE_FULL";
+ case 10: return "ATTRIBUTE_NOT_FOUND";
+ case 11: return "ATTRIBUTE_NOT_LONG";
+ case 12: return "INSUFFICIENT_ENCRYPTION_KEY_SIZE";
+ case 14: return "UNLIKELY_ERROR";
+ case 16: return "UNSUPPORTED_GROUP_TYPE";
+ case 17: return "INSUFFICIENT_RESOURCES";
+ default: return "unknown error status: " + status;
+ }
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattOperation.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattOperation.java
new file mode 100644
index 0000000..6fe64f6
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattOperation.java
@@ -0,0 +1,24 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig.gatt;
+
+/**
+ * Supported GATT operations.
+ */
+public class GattOperation {
+ private GattOperation() {}
+ public static final int OP_READ = 1;
+ public static final int OP_WRITE = 2;
+}
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattOperationException.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattOperationException.java
new file mode 100644
index 0000000..75e2937
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/gatt/GattOperationException.java
@@ -0,0 +1,61 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package com.github.google.beaconfig.gatt;
+
+import com.github.google.beaconfig.utils.Utils;
+
+import java.util.Locale;
+import java.util.UUID;
+
+/**
+ * Basic wrapper for an error during a GATT operation.
+ */
+public class GattOperationException extends Exception {
+ // Errors beyond the canonical errors in the BluetoothGatt class.
+ public static final int ERROR_INTERRUPTED = -1;
+ public static final int ERROR_EXECUTION = -2;
+ public static final int ERROR_TIMEOUT = -3;
+ public final UUID uuid;
+ public final int op;
+ public final int status;
+ public final byte[] data;
+ /**
+ * Wraps a GATT error from a completed operation that returned a canonical error code.
+ * @param uuid the UUID of the characteristic that triggered the error.
+ * @param op the operation that triggered the error, defined by one of the constants in
+ * the GattOperation class.
+ * @param status maps to one of the constants defined in the platform's BluetoothGatt class.
+ * @param data the data returned by the operation.
+ */
+ public GattOperationException(UUID uuid, int op, int status, byte[] data) {
+ this.uuid = uuid;
+ this.op = op;
+ this.status = status;
+ this.data = data;
+ }
+ @Override
+ public String getMessage() {
+ String opStr = op == GattOperation.OP_READ ? "reading" : "writing";
+ String dataStr
+ = data == null ? "null" : data.length == 0 ? "[empty]" : Utils.toHexString(data);
+ String message = String.format(Locale.UK,
+ "GATT Error %s when %s characteristic %s, data: %s",
+ GattConstants.getStatusString(status),
+ opStr,
+ GattConstants.getReadableName(uuid),
+ dataStr);
+ return message;
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/BroadcastCapabilities.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/BroadcastCapabilities.java
new file mode 100644
index 0000000..7119a2d
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/BroadcastCapabilities.java
@@ -0,0 +1,97 @@
+package com.github.google.beaconfig.utils;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+import com.github.google.beaconfig.gatt.GattConstants;
+
+/**
+ * Class to parse and hold all the broadcast capabilities of a beacon
+ */
+public class BroadcastCapabilities {
+ private int version;
+ private int maxSupportedTotalSlots;
+ private int maxSupportedEidSlots;
+ private byte[] supportedTxPowers;
+
+ private boolean isVariableTxPowerSupported;
+ private boolean isVariableAdvSupported;
+
+ private boolean isUidSupported;
+ private boolean isUrlSupported;
+ private boolean isTlmSupported;
+ private boolean isEidSupported;
+
+ public BroadcastCapabilities(byte[] data) {
+ ByteBuffer buf = ByteBuffer.wrap(data);
+ version = buf.get();
+ maxSupportedTotalSlots = (int) buf.get();
+ maxSupportedEidSlots = (int) buf.get();
+
+ byte capabilitiesBitField = buf.get();
+ isVariableAdvSupported =
+ (capabilitiesBitField & GattConstants.CAPABILITIES_IS_VARIABLE_ADV_SUPPORTED)
+ == GattConstants.CAPABILITIES_IS_VARIABLE_ADV_SUPPORTED;
+ isVariableTxPowerSupported =
+ (capabilitiesBitField
+ & GattConstants.CAPABILITIES_IS_VARIABLE_TX_POWER_SUPPORTED)
+ == GattConstants.CAPABILITIES_IS_VARIABLE_TX_POWER_SUPPORTED;
+
+ short supportedFrameTypesBitField = buf.getShort();
+ isUidSupported =
+ (supportedFrameTypesBitField & GattConstants.CAPABILITIES_IS_UID_FRAME_SUPPORTED)
+ == GattConstants.CAPABILITIES_IS_UID_FRAME_SUPPORTED;
+ isUrlSupported =
+ (supportedFrameTypesBitField & GattConstants.CAPABILITIES_IS_URL_FRAME_SUPPORTED)
+ == GattConstants.CAPABILITIES_IS_URL_FRAME_SUPPORTED;
+ isTlmSupported =
+ (supportedFrameTypesBitField & GattConstants.CAPABILITIES_IS_TLM_FRAME_SUPPORTED)
+ == GattConstants.CAPABILITIES_IS_TLM_FRAME_SUPPORTED;
+ isEidSupported =
+ (supportedFrameTypesBitField & GattConstants.CAPABILITIES_IS_EID_FRAME_SUPPORTED)
+ == GattConstants.CAPABILITIES_IS_EID_FRAME_SUPPORTED;
+
+ supportedTxPowers = Arrays.copyOfRange(data, buf.position(), data.length);
+ Arrays.sort(supportedTxPowers);
+ }
+
+ public int getVersion() {
+ return version;
+ }
+
+ public int getMaxSupportedTotalSlots() {
+ return maxSupportedTotalSlots;
+ }
+
+ public int getMaxSupportedEidSlots() {
+ return maxSupportedEidSlots;
+ }
+
+ public byte[] getSupportedTxPowers() {
+ return supportedTxPowers;
+ }
+
+ public boolean isVariableTxPowerSupported() {
+ return isVariableTxPowerSupported;
+ }
+
+ public boolean isVariableAdvSupported() {
+ return isVariableAdvSupported;
+ }
+
+ public boolean isUidSupported() {
+ return isUidSupported;
+ }
+
+ public boolean isUrlSupported() {
+ return isUrlSupported;
+ }
+
+ public boolean isTlmSupported() {
+ return isTlmSupported;
+ }
+
+ public boolean isEidSupported() {
+ return isEidSupported;
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/SlotDataManager.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/SlotDataManager.java
new file mode 100644
index 0000000..549bc64
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/SlotDataManager.java
@@ -0,0 +1,212 @@
+package com.github.google.beaconfig.utils;
+
+import android.util.Log;
+
+import java.util.Arrays;
+
+import com.github.google.beaconfig.Constants;
+
+/**
+ * This class is composed of static methods to be used to either parse information from given
+ * slotData or to build slotData from given strings and integers.
+ */
+public class SlotDataManager {
+ private static final String TAG = SlotDataManager.class.getSimpleName();
+
+ public static byte getFrameTypeFromSlotData(byte[] slotData) {
+ return slotData[0];
+ }
+
+ /**
+ * @param slotData has to be the slot data read from a UID frame of a beacon
+ * @return namespace String parsed from the slot data byte array
+ */
+ public static String getNamespaceFromSlotData(byte[] slotData) {
+ if (getFrameTypeFromSlotData(slotData) != Constants.UID_FRAME_TYPE) {
+ Log.d(TAG, "Attempt to get namespace from a non-uid frame failed.");
+ return "";
+ }
+ byte[] namespaceBytes = Arrays.copyOfRange(slotData, 2, 12);
+ return Utils.byteArrayToHexString(namespaceBytes);
+ }
+
+ /**@param writeSlotData has to be the byte array to be written to a beacon slot in order to
+ * configure it as a UID slot
+ * @return namespace String parsed from the write byte array
+ */
+ public static String getNamespaceFromWriteByteArray(byte[] writeSlotData) {
+ if (getFrameTypeFromSlotData(writeSlotData) != Constants.UID_FRAME_TYPE) {
+ Log.d(TAG, "Attempt to get namespace from a non-uid frame failed.");
+ return "";
+ }
+ byte[] namespaceBytes = Arrays.copyOfRange(writeSlotData, 1, 11);
+ return Utils.byteArrayToHexString(namespaceBytes);
+ }
+
+ /**
+ * @param slotData has to be the slot data of a UID configured slot of a beacon
+ * @return instance String parsed from the slot data byte array
+ */
+ public static String getInstanceFromSlotData(byte[] slotData) {
+ if (getFrameTypeFromSlotData(slotData) != Constants.UID_FRAME_TYPE) {
+ Log.d(TAG, "Attempt to get instance from a non-uid frame failed.");
+ return "";
+ }
+ byte[] instanceBytes = Arrays.copyOfRange(slotData, 12, 18);
+ return Utils.byteArrayToHexString(instanceBytes);
+ }
+
+ /**
+ * @param writeSlotData has to be the byte array to be written to a beacon slot in order to
+ * configure it as a UID slot
+ * @return namespace String parsed from the write byte array
+ */
+ public static String getInstanceFromWriteByteArray(byte[] writeSlotData) {
+ if (getFrameTypeFromSlotData(writeSlotData) != Constants.UID_FRAME_TYPE) {
+ Log.d(TAG, "Attempt to get instance from a non-uid frame failed.");
+ return "";
+ }
+ byte[] instanceBytes = Arrays.copyOfRange(writeSlotData, 11, 17);
+ return Utils.byteArrayToHexString(instanceBytes);
+ }
+
+ /**
+ * @param slotData has to be the slot data of a URL configured slot of a beacon
+ * @return url as a String parsed from the slot data byte array
+ */
+ public static String getUrlFromSlotData(byte[] slotData) {
+ if (getFrameTypeFromSlotData(slotData) != Constants.URL_FRAME_TYPE) {
+ Log.d(TAG, "Attempt to get URL from a non-url frame failed.");
+ return "";
+ }
+ return URLEncodeDecoder.decodeUrlFromUrlBytes(
+ Arrays.copyOfRange(slotData, 2, slotData.length));
+ }
+
+ /**
+ * @param writeSlotData has to be the byte array to be written to a beacon slot in order to
+ * configure it as a URL slot
+ * @return url as a String parsed from the write byte array
+ */
+ public static String getUrlFromWriteByteArray(byte[] writeSlotData) {
+ if (getFrameTypeFromSlotData(writeSlotData) != Constants.URL_FRAME_TYPE) {
+ Log.d(TAG, "Attempt to get URL from a non-url frame failed.");
+ return "";
+ }
+ return URLEncodeDecoder.decodeUrlFromUrlBytes(
+ Arrays.copyOfRange(writeSlotData, 1, writeSlotData.length));
+ }
+
+ /**
+ * @param slotData has to be the slot data of a TLM configured slot of a beacon
+ * @return version parsed from the slot data byte array
+ */
+ public static byte getVersionFromSlotData(byte[] slotData) {
+ if (getFrameTypeFromSlotData(slotData) != Constants.TLM_FRAME_TYPE) {
+ Log.d(TAG, "Attempt to get version from a non-tlm frame failed.");
+ return 0;
+ }
+ return slotData[1];
+ }
+
+ /**
+ * @param slotData has to be the slot data of a TLM configured slot of a beacon
+ * @return voltage parsed from the slot data byte array
+ */
+ public static short getVoltageFromSlotData(byte[] slotData) {
+ if (getFrameTypeFromSlotData(slotData) != Constants.TLM_FRAME_TYPE) {
+ Log.d(TAG, "Attempt to get voltage from a non-tlm frame failed.");
+ return 0;
+ }
+ return (short) (((slotData[2] & 0xff) << 8) | (slotData[3] & 0xff));
+ }
+
+ /**
+ * @param slotData has to be the slot data of a TLM configured slot of a beacon
+ * @return temperature parsed from the slot data byte array
+ */
+ public static float getTemperatureFromSlotData(byte[] slotData) {
+ if (getFrameTypeFromSlotData(slotData) != Constants.TLM_FRAME_TYPE) {
+ Log.d(TAG, "Attempt to get temperature from a non-tlm frame failed.");
+ return 0;
+ }
+ byte tempIntegral = slotData[4];
+ int tempFractional = (slotData[5] & 0xff);
+ return tempIntegral + (tempFractional / 256.0f);
+ }
+
+ /**
+ * @param slotData has to be the slot data of a TLM configured slot of a beacon
+ * @return advertising PDU count parsed from the slot data byte array
+ */
+ public static int getAdvertisingPDUCountFromSlotData(byte[] slotData) {
+ if (getFrameTypeFromSlotData(slotData) != Constants.TLM_FRAME_TYPE) {
+ Log.d(TAG, "Attempt to get advertising PDU count from a non-tlm frame failed.");
+ return 0;
+ }
+ return ((slotData[6] & 0xff) << 24)
+ | ((slotData[7] & 0xff) << 16)
+ | ((slotData[8] & 0xff) << 8)
+ | (slotData[9] & 0xff);
+ }
+
+ /**
+ * @param slotData TLM slot data read from beacon
+ * @return time since on in milliseconds
+ */
+ public static int getTimeSinceOnFromSlotData(byte[] slotData) {
+ if (getFrameTypeFromSlotData(slotData) != Constants.TLM_FRAME_TYPE) {
+ Log.d(TAG, "Attempt to get time since on from a non-tlm frame failed.");
+ return 0;
+ }
+ return ((slotData[10] & 0xff) << 24)
+ | ((slotData[11] & 0xff) << 16)
+ | ((slotData[12] & 0xff) << 8)
+ | (slotData[13] & 0xff);
+ }
+
+ public static String getEphemeralIdFromSlotData(byte[] slotData) {
+ if (getFrameTypeFromSlotData(slotData) != Constants.EID_FRAME_TYPE) {
+ Log.d(TAG, "Attempt to get ephemeral id from a non-eid frame failed.");
+ return "";
+ }
+ return Utils.byteArrayToHexString(Arrays.copyOfRange(slotData, 2, 10));
+ }
+
+
+ public static byte[] buildNewUidSlotData(String namespace, String instance) {
+ byte[] newUidSlotData = new byte[17];
+
+ newUidSlotData[0] = Constants.UID_FRAME_TYPE;
+
+ if (namespace != null && namespace.length() == 20) {
+ byte[] namespaceBytes = Utils.toByteArray(namespace);
+ newUidSlotData = Utils.rewriteBytes(newUidSlotData, 1, 10, namespaceBytes);
+ }
+
+ if (instance != null && instance.length() == 12) {
+ byte[] instanceBytes = Utils.toByteArray(instance);
+ newUidSlotData = Utils.rewriteBytes(newUidSlotData, 11, 6, instanceBytes);
+ }
+
+ return newUidSlotData;
+ }
+
+ public static byte[] buildNewUrlSlotData(String newUrl) {
+ byte[] urlBytes = URLEncodeDecoder.encodeUri(newUrl);
+ byte[] newSlotData = new byte[urlBytes.length + 1];
+ newSlotData[0] = Constants.URL_FRAME_TYPE;
+ newSlotData = Utils.rewriteBytes(newSlotData, 1, urlBytes.length, urlBytes);
+ return newSlotData;
+ }
+
+ public static byte[] buildNewTlmSlotData() {
+ byte[] newSlotData = new byte[1];
+ newSlotData[0] = Constants.TLM_FRAME_TYPE;
+ return newSlotData;
+ }
+
+ public static byte[] buildNewEidSlotData() {
+ return new byte[0];
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/URLEncodeDecoder.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/URLEncodeDecoder.java
new file mode 100644
index 0000000..a381e88
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/URLEncodeDecoder.java
@@ -0,0 +1,195 @@
+package com.github.google.beaconfig.utils;
+
+import android.util.Log;
+import android.util.SparseArray;
+import android.webkit.URLUtil;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Locale;
+import java.util.UUID;
+
+/**
+ * Manages decoding of URL from a url service data.
+ */
+public class URLEncodeDecoder {
+ private static final String TAG = URLEncodeDecoder.class.getSimpleName();
+
+ private static final SparseArray URI_SCHEMES = new SparseArray() {{
+ put((byte) 0, "http://www.");
+ put((byte) 1, "https://www.");
+ put((byte) 2, "http://");
+ put((byte) 3, "https://");
+ put((byte) 4, "urn:uuid:");
+ }};
+
+ private static final SparseArray URL_CODES = new SparseArray() {{
+ put((byte) 0, ".com/");
+ put((byte) 1, ".org/");
+ put((byte) 2, ".edu/");
+ put((byte) 3, ".net/");
+ put((byte) 4, ".info/");
+ put((byte) 5, ".biz/");
+ put((byte) 6, ".gov/");
+ put((byte) 7, ".com");
+ put((byte) 8, ".org");
+ put((byte) 9, ".edu");
+ put((byte) 10, ".net");
+ put((byte) 11, ".info");
+ put((byte) 12, ".biz");
+ put((byte) 13, ".gov");
+ }};
+
+ public static String decodeUrlFromUrlBytes(byte[] serviceData) {
+ if (serviceData == null || (serviceData != null && serviceData.length == 0)) {
+ return null;
+ }
+ StringBuilder url = new StringBuilder();
+ int offset = 0;
+ byte b = serviceData[offset++];
+ String scheme = URI_SCHEMES.get(b);
+ if (scheme != null) {
+ url.append(scheme); // too cramped in UI to show scheme.
+ if (URLUtil.isNetworkUrl(scheme)) {
+ return decodeUrl(serviceData, offset, url);
+ } else if ("urn:uuid:".equals(scheme)) {
+ return decodeUrnUuid(serviceData, offset, url);
+ }
+ }
+ return url.toString();
+ }
+
+ public static String decodeUrl(byte[] serviceData, int offset, StringBuilder urlBuilder) {
+ while (offset < serviceData.length) {
+ byte b = serviceData[offset++];
+ String code = URL_CODES.get(b);
+ if (code != null) {
+ urlBuilder.append(code);
+ } else {
+ urlBuilder.append((char) b);
+ }
+ }
+ return urlBuilder.toString();
+ }
+
+ static String decodeUrnUuid(byte[] serviceData, int offset, StringBuilder urnBuilder) {
+ ByteBuffer bb = ByteBuffer.wrap(serviceData);
+ // UUIDs are ordered as byte array, which means most significant first
+ bb.order(ByteOrder.BIG_ENDIAN);
+ long mostSignificantBytes, leastSignificantBytes;
+ try {
+ bb.position(offset);
+ mostSignificantBytes = bb.getLong();
+ leastSignificantBytes = bb.getLong();
+ } catch (BufferUnderflowException e) {
+ Log.d(TAG, "Decoding UrnUuid failed.");
+ return null;
+ }
+ UUID uuid = new UUID(mostSignificantBytes, leastSignificantBytes);
+ urnBuilder.append(uuid.toString());
+ return urnBuilder.toString();
+ }
+
+ /**
+ * Creates the Uri string with embedded expansion codes.
+ *
+ * @param uri to be encoded
+ * @return the Uri string with expansion codes.
+ */
+ public static byte[] encodeUri(String uri) {
+ if (uri.length() == 0) {
+ return new byte[0];
+ }
+ ByteBuffer bb = ByteBuffer.allocate(uri.length());
+ // UUIDs are ordered as byte array, which means most significant first
+ bb.order(ByteOrder.BIG_ENDIAN);
+ int position = 0;
+
+ // Add the byte code for the scheme or return null if none
+ Byte schemeCode = encodeUriScheme(uri);
+ if (schemeCode == null) {
+ return null;
+ }
+ String scheme = URI_SCHEMES.get(schemeCode);
+ bb.put(schemeCode);
+ position += scheme.length();
+
+ if (URLUtil.isNetworkUrl(scheme)) {
+ return encodeUrl(uri, position, bb);
+ } else if ("urn:uuid:".equals(scheme)) {
+ return encodeUrnUuid(uri, position, bb);
+ }
+ return null;
+ }
+
+ private static Byte encodeUriScheme(String uri) {
+ String lowerCaseUri = uri.toLowerCase(Locale.ENGLISH);
+ for (int i = 0; i < URI_SCHEMES.size(); i++) {
+ // get the key and value.
+ int key = URI_SCHEMES.keyAt(i);
+ String value = URI_SCHEMES.valueAt(i);
+ if (lowerCaseUri.startsWith(value)) {
+ return (byte) key;
+ }
+ }
+ return null;
+ }
+
+ private static byte[] encodeUrl(String url, int position, ByteBuffer bb) {
+ while (position < url.length()) {
+ byte expansion = findLongestExpansion(url, position);
+ if (expansion >= 0) {
+ bb.put(expansion);
+ position += URL_CODES.get(expansion).length();
+ } else {
+ bb.put((byte) url.charAt(position++));
+ }
+ }
+ return byteBufferToArray(bb);
+ }
+
+ private static byte[] encodeUrnUuid(String urn, int position, ByteBuffer bb) {
+ String uuidString = urn.substring(position, urn.length());
+ UUID uuid;
+ try {
+ uuid = UUID.fromString(uuidString);
+ } catch (IllegalArgumentException e) {
+ Log.w("TAG", "encodeUrnUuid invalid urn:uuid format - " + urn);
+ return null;
+ }
+ // UUIDs are ordered as byte array, which means most significant first
+ bb.order(ByteOrder.BIG_ENDIAN);
+ bb.putLong(uuid.getMostSignificantBits());
+ bb.putLong(uuid.getLeastSignificantBits());
+ return byteBufferToArray(bb);
+ }
+
+ /**
+ * Finds the longest expansion from the uri at the current position.
+ *
+ * @param uriString the Uri
+ * @param pos start position
+ * @return an index in URI_MAP or 0 if none.
+ */
+ private static byte findLongestExpansion(String uriString, int pos) {
+ byte expansion = -1;
+ int expansionLength = 0;
+ for (int i = 0; i < URL_CODES.size(); i++) {
+ // get the key and value.
+ int key = URL_CODES.keyAt(i);
+ String value = URL_CODES.valueAt(i);
+ if (value.length() > expansionLength && uriString.startsWith(value, pos)) {
+ expansion = (byte) key;
+ expansionLength = value.length();
+ }
+ }
+ return expansion;
+ }
+
+ private static byte[] byteBufferToArray(ByteBuffer bb) {
+ byte[] bytes = new byte[bb.position()];
+ bb.rewind();
+ bb.get(bytes, 0, bytes.length);
+ return bytes;
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/UiUtils.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/UiUtils.java
new file mode 100644
index 0000000..ce3e3db
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/UiUtils.java
@@ -0,0 +1,53 @@
+package com.github.google.beaconfig.utils;
+
+import android.app.Activity;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+/**
+ * Static methods helpful throughout the program.
+ */
+public class UiUtils {
+
+ public static void showToast(final Activity activity, final String message) {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ Toast toast = Toast.makeText(activity.getApplicationContext(),
+ message, Toast.LENGTH_SHORT);
+ toast.setGravity(Gravity.CENTER_VERTICAL | Gravity.BOTTOM, 0, 80);
+ toast.show();
+ }
+ });
+ }
+
+ public static void makeChildrenInvisible(ViewGroup view) {
+ for (int i = 0; i < view.getChildCount(); i++) {
+ view.getChildAt(i).setVisibility(View.GONE);
+ }
+ }
+
+ public static void enableAllChildren(View view) {
+ if (view instanceof ViewGroup) {
+ ViewGroup viewGroup = (ViewGroup) view;
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
+ View child = viewGroup.getChildAt(i);
+ child.setEnabled(true);
+ enableAllChildren(child);
+ }
+ }
+ }
+
+ public static void disableAllChildren(View view) {
+ if (view instanceof ViewGroup) {
+ ViewGroup viewGroup = (ViewGroup) view;
+ for (int i = 0; i < viewGroup.getChildCount(); i++) {
+ View child = viewGroup.getChildAt(i);
+ child.setEnabled(false);
+ disableAllChildren(child);
+ }
+ }
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/Utils.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/Utils.java
new file mode 100644
index 0000000..11d13fa
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/com/github/google/beaconfig/utils/Utils.java
@@ -0,0 +1,266 @@
+package com.github.google.beaconfig.utils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.util.Log;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+
+import com.github.google.beaconfig.BeaconConfigActivity;
+import com.github.google.beaconfig.Constants;
+import com.github.google.beaconfig.R;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * Helper functions throughout the program.
+ */
+
+public class Utils {
+ private static final String TAG = BeaconConfigActivity.class.getSimpleName();
+
+ private static final char[] HEX = "0123456789ABCDEF".toCharArray();
+
+ public static String byteArrayToHexString(byte[] bytes) {
+ if (bytes == null) {
+ Log.d(TAG, "Error. byteArrayToHexString() - input null");
+ return "";
+ }
+ char[] charArray = new char[bytes.length * 2];
+ int charIndex = 0;
+ for (byte currentByte : bytes) {
+ charArray[charIndex++] = HEX[(currentByte >> 4) & 0x0f];
+ charArray[charIndex++] = HEX[currentByte & 0x0f];
+ }
+ return new String(charArray);
+ }
+
+ public static byte[] toByteArray(String s) {
+ // s guaranteed valid by caller.
+ int len = s.length();
+ byte[] bytes = new byte[len / 2];
+ for (int i = 0; i < len; i += 2) {
+ bytes[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ + Character.digit(s.charAt(i + 1), 16));
+ }
+ return bytes;
+ }
+
+ public static byte[] aes128Encrypt(byte[] challenge, byte[] unlockCode) {
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance("AES/ECB/NoPadding");
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Error constructing cipher instance", e);
+ return null;
+ }
+ try {
+ SecretKeySpec keySpec = new SecretKeySpec(unlockCode, "AES");
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec);
+ } catch (InvalidKeyException e) {
+ Log.e(TAG, "Error initializing cipher instance", e);
+ return null;
+ }
+ byte[] ret;
+ try {
+ ret = cipher.doFinal(challenge);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Error executing cipher", e);
+ return null;
+ }
+ return ret;
+ }
+
+ public static String toHexString(byte[] bytes) {
+ if (bytes == null || bytes.length == 0) {
+ return "";
+ }
+ char[] chars = new char[bytes.length * 2];
+ for (int i = 0; i < bytes.length; i++) {
+ int c = bytes[i] & 0xFF;
+ chars[i * 2] = HEX[c >>> 4];
+ chars[i * 2 + 1] = HEX[c & 0x0F];
+ }
+ return new String(chars).toLowerCase();
+ }
+
+ public static byte[] toTwoByteArray(int i) {
+ byte[] out = new byte[2];
+ out[0] = (byte) ((i >> 8) & 0xFF);
+ out[1] = (byte) (i & 0xFF);
+ return out;
+ }
+
+ public static void sleep(int millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (InterruptedException e) {
+ Log.e(TAG, "sleep interruption", e);
+ }
+ }
+
+ public static boolean isZeroed(byte[] bytes) {
+ for (byte b : bytes) {
+ if (b != 0x00) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // Caller guarantees data is two bytes.
+ public static int toInt(byte[] data) {
+ return ((data[0] & 0xff) << 8 | data[1] & 0xff);
+ }
+
+ public static boolean isHex(char c) {
+ return ((c >= '0') && (c <= '9'))
+ || ((c >= 'a') && (c <= 'f'))
+ || ((c >= 'A') && (c <= 'F'));
+ }
+
+ public static byte findMaxValue(byte[] array) {
+ byte max = array[0];
+
+ for (byte a : array) {
+ if (a > max) {
+ max = a;
+ }
+ }
+ return max;
+ }
+
+ public static byte findMinValue(byte[] array) {
+ byte max = array[0];
+
+ for (byte a : array) {
+ if (a < max) {
+ max = a;
+ }
+ }
+ return max;
+ }
+
+ public static int findValueClosestTo(int value, byte[] allowedValues) {
+ int smallestDistance = Math.abs(allowedValues[0] - value);
+ int index = 0;
+ for (int i = 1; i < allowedValues.length; i++) {
+ int currDistance = Math.abs(allowedValues[i] - value);
+ if (currDistance < smallestDistance) {
+ index = i;
+ smallestDistance = currDistance;
+ }
+ }
+ return allowedValues[index];
+ }
+
+ public static byte[] rewriteBytes(byte[] original, int beginIndex,
+ int howMany, byte[] replaceWith) {
+ for (int i = beginIndex; i < beginIndex + howMany; i++) {
+ original[i] = replaceWith[i - beginIndex];
+ }
+
+ return original;
+ }
+
+ public static String getStringFromFrameType(byte frameType) {
+ switch (frameType) {
+ case Constants.UID_FRAME_TYPE:
+ return Constants.UID;
+
+ case Constants.URL_FRAME_TYPE:
+ return Constants.URL;
+
+ case Constants.TLM_FRAME_TYPE:
+ return Constants.TLM;
+
+ case Constants.EID_FRAME_TYPE:
+ return Constants.EID;
+
+ default:
+ return "--";
+ }
+ }
+
+ public static byte getFrameTypeFromString(String frameString) {
+ switch (frameString) {
+ case Constants.UID:
+ return Constants.UID_FRAME_TYPE;
+
+ case Constants.URL:
+ return Constants.URL_FRAME_TYPE;
+
+ case Constants.TLM:
+ return Constants.TLM_FRAME_TYPE;
+
+ case Constants.EID:
+ return Constants.EID_FRAME_TYPE;
+
+ default:
+ return Constants.EMPTY_FRAME_TYPE;
+ }
+ }
+
+ public static String getFrameNameFromSlotData(byte[] slotData) {
+ if (slotIsEmpty(slotData)) {
+ return "--";
+ } else {
+ return getStringFromFrameType(slotData[0]);
+ }
+ }
+
+ public static boolean slotIsEmpty(byte[] slotData) {
+ return slotData.length < 2;
+ }
+
+ /**
+ * @param uptime has to be in milliseconds
+ * @return String representation rounded down to seconds, minutes, hours or days
+ */
+ @NonNull
+ public static String getTimeString(int uptime) {
+ int uptimeSecs = uptime / 10;
+ String timeOn;
+ if (TimeUnit.SECONDS.toMinutes(uptimeSecs) == 0) {
+ timeOn = uptimeSecs + (uptimeSecs == 1 ? " sec" : " secs");
+ } else if (TimeUnit.SECONDS.toHours(uptimeSecs) == 0) {
+ long uptimeMins = TimeUnit.SECONDS.toMinutes(uptimeSecs);
+ timeOn = uptimeMins + (uptimeMins == 1 ? " min" : " mins");
+ } else if (TimeUnit.SECONDS.toDays(uptimeSecs) == 0) {
+ long uptimeHours = TimeUnit.SECONDS.toHours(uptimeSecs);
+ timeOn = uptimeHours + (uptimeHours == 1 ? " hour" : " hours");
+ } else {
+ long uptimeDays = TimeUnit.SECONDS.toDays(uptimeSecs);
+ timeOn = uptimeDays + (uptimeDays == 1 ? " day" : " days");
+ }
+ return timeOn;
+ }
+
+ public static void hideKeyboard(Activity activity, View view) {
+ InputMethodManager imm
+ = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+
+ public static String[] generateArrayWithStringValuesInRange(int min, int max) {
+ ArrayList arrayList = new ArrayList<>();
+ for (int i = min; i < max; i++) {
+ arrayList.add(Integer.toString(i));
+ }
+ String[] stringArray = new String[arrayList.size()];
+ for (int i = 0; i < arrayList.size(); i++) {
+ stringArray[i] = arrayList.get(i);
+ }
+ return stringArray;
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/layout/BeaconTabFragment.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/layout/BeaconTabFragment.java
new file mode 100644
index 0000000..633042d
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/layout/BeaconTabFragment.java
@@ -0,0 +1,352 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package layout;
+
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import com.github.google.beaconfig.Constants;
+import com.github.google.beaconfig.R;
+import com.github.google.beaconfig.dialogs.ConfirmationDialog;
+import com.github.google.beaconfig.utils.BroadcastCapabilities;
+import com.github.google.beaconfig.utils.UiUtils;
+import com.github.google.beaconfig.utils.Utils;
+
+/**
+ * Abstract class to unite common behaviour between global fragment (displaying features of the
+ * beacon) and slot fragments (displaying features of each particular slot of the beacon).
+ */
+public abstract class BeaconTabFragment extends Fragment {
+ public String name;
+ protected ConfigurationListener configurationListener;
+ protected ExecutorService executor;
+
+ private String txPower;
+ private String advTxPower;
+ private String advInterval;
+ protected int slotNumber;
+ protected BroadcastCapabilities capabilities;
+
+ private boolean txPowerChanged;
+ private boolean advTxPowerChanged;
+ private boolean advIntervalChanged;
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+ executor = Executors.newSingleThreadExecutor();
+ configurationListener = (ConfigurationListener) getActivity();
+
+ updateInformation(getArguments());
+ setHasOptionsMenu(true);
+ }
+
+ /**
+ * Updates slot information by pulling values from the bundle. These values can be:
+ * - broadcast capabilities type: byte[] key: BROADCAST_CAPABILITIES
+ * - radio tx power type: String key: TX_POWER
+ * - adv tx power type: String key: ADV_POWER
+ * - adv interval type: String key: ADV_INTERVAL
+ *
+ * @param bundle bundle containing all the information to be updated
+ */
+ public void updateInformation(Bundle bundle) {
+ byte[] data = bundle.getByteArray(Constants.BROADCAST_CAPABILITIES);
+ if (data != null) {
+ capabilities = new BroadcastCapabilities(data);
+ }
+
+ String txPower = bundle.getString(Constants.TX_POWER);
+ if (txPower != null) {
+ this.txPower = txPower;
+ }
+
+ String advTxPower = bundle.getString(Constants.ADV_POWER);
+ if (advTxPower != null) {
+ this.advTxPower = advTxPower;
+ }
+
+ String advInterval = bundle.getString(Constants.ADV_INTERVAL);
+ if (advInterval != null) {
+ this.advInterval = advInterval;
+ }
+ }
+
+ protected void setUpFragment(View v) {
+ setUpRadioTxPower(v);
+ setUpAdvertisedTxPower(v);
+ setUpAdvertisingInterval(v);
+ }
+
+ /**
+ * Sets up the advertising tx power view. It is initially disabled. It is enabled by clicking
+ * on the radio button in the UI. This opens a dialog to confirm that the user really wants to
+ * change the advertising radio tx power. After this the advertising interval can be configured.
+ * A list pops up where the user can choose which value they want to be broadcast as the
+ * advertising tx power.
+ */
+ private void setUpAdvertisedTxPower(final View v) {
+ if (advTxPower != null) {
+ LinearLayout advTxPowerContainer
+ = (LinearLayout) v.findViewById(R.id.change_adv_tx_slot);
+
+ final TextView advTxPowerView = (TextView) v.findViewById(R.id.adv_tx_power);
+ advTxPowerView.setText(advTxPower);
+ advTxPowerView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View view, boolean hasFocus) {
+ if (!hasFocus) {
+ String newAdvTxPower = advTxPowerView.getText().toString();
+ if (newAdvTxPower.length() == 0) {
+ advTxPowerView.setText(advTxPower);
+ }
+
+ } else {
+ advTxPowerChanged = true;
+ }
+ }
+ });
+
+ advTxPowerContainer.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ advTxPowerChanged = true;
+ final String[] possibleValues = Utils.generateArrayWithStringValuesInRange(
+ Utils.findMinValue(capabilities.getSupportedTxPowers()) - 5,
+ Utils.findMaxValue(capabilities.getSupportedTxPowers()) + 5);
+ new AlertDialog.Builder(getActivity())
+ .setTitle("Choose advertising tx power:")
+ .setItems(possibleValues, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialogInterface, int i) {
+ advTxPower = possibleValues[i];
+ advTxPowerView.setText(possibleValues[i]);
+ }
+ })
+ .show();
+ }
+ });
+
+ UiUtils.disableAllChildren(advTxPowerContainer);
+
+ final RadioButton enableChangeAdvTxPowerButton
+ = (RadioButton) v.findViewById(R.id.change_slot_adv_tx_power);
+ enableChangeAdvTxPowerButton.setChecked(false);
+ enableChangeAdvTxPowerButton.setOnClickListener(new View.OnClickListener() {
+ private boolean enabled = false;
+ @Override
+ public void onClick(View view) {
+ if (enabled) {
+ UiUtils.disableAllChildren(v.findViewById(R.id.change_adv_tx_slot));
+ enableChangeAdvTxPowerButton.setChecked(false);
+ enabled = false;
+ } else {
+ ConfirmationDialog.confirm("Change Advertising Interval", "Are you sure "
+ + "you want to change the advertising interval of this beacon?",
+ "YES", "NO", getActivity(),
+ new ConfirmationDialog.ConfirmationListener() {
+ @Override
+ public void confirm() {
+ UiUtils.enableAllChildren(
+ v.findViewById(R.id.change_adv_tx_slot));
+ enableChangeAdvTxPowerButton.setChecked(true);
+ enabled = true;
+ }
+
+ @Override
+ public void cancel() {
+ enableChangeAdvTxPowerButton.setChecked(false);
+ enabled = false;
+ }
+ });
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Setting up the radio tx power component with a seek bar which reads the supported tx powers
+ * and only allows the user to set these. There is also a tracking TextView which shows the
+ * numeric representation of the tx power.
+ *
+ * @param v container for the radio tx power component. This is usually the main container in
+ * the tab. It has to be added to the activity before calling this method.
+ */
+ private void setUpRadioTxPower(View v) {
+ if (txPower != null) {
+ v.findViewById(R.id.tx_power_info).setVisibility(View.VISIBLE);
+
+ final TextView txPowerView = (TextView) v.findViewById(R.id.radio_tx_power);
+ txPowerView.setText(txPower);
+ SeekBar seekBar = (SeekBar) v.findViewById(R.id.tx_power_seek_bar);
+
+ final byte[] allowedValues = capabilities.getSupportedTxPowers();
+ final int maxValue = Utils.findMaxValue(allowedValues);
+ final int minValue = Utils.findMinValue(allowedValues);
+
+ seekBar.setMax(maxValue - minValue);
+ seekBar.setProgress(Integer.parseInt(txPower) - minValue);
+
+ seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ int newValue;
+ if (allowedValues != null) {
+ newValue = Utils.findValueClosestTo(progress + minValue, allowedValues);
+ } else {
+ newValue = progress + minValue;
+ }
+ seekBar.setProgress(newValue - minValue);
+ txPowerView.setText(Integer.toString(newValue));
+ BeaconTabFragment.this.txPower = Integer.toString(newValue);
+ }
+ });
+ }
+ }
+
+ /**
+ * Setting up the advertising interval component with a seek bar which can be set to values from
+ * 100 to 1024. There is also a tracking TextView which shows the numeric representation of the
+ * advertising interval.
+ *
+ * @param v container for the advertising interval component. This is usually the main container
+ * in the tab. It has to be added to the activity before calling this method.
+ */
+ private void setUpAdvertisingInterval(View v) {
+ if (advInterval != null) {
+
+ v.findViewById(R.id.adv_int_info).setVisibility(View.VISIBLE);
+ final TextView advIntervalView = (TextView) v.findViewById(R.id.adv_interval);
+ advIntervalView.setText(advInterval);
+ SeekBar advInterSeekBar = (SeekBar) v.findViewById(R.id.adv_interval_seek_bar);
+ advInterSeekBar.setMax(1024 - 100);
+ int progress = Integer.parseInt(advInterval) - 100;
+ advInterSeekBar.setProgress(progress);
+ advInterSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int i, boolean b) {
+ int progress = seekBar.getProgress() + 100;
+ advIntervalView.setText(Integer.toString(progress));
+ advIntervalChanged = true;
+ advInterval = advIntervalView.getText().toString();
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ });
+ }
+ }
+
+ /**
+ * Reads the state variables txPowerChanged, advTxPowerChanged and advIntervalChanged and
+ * uses the ConfigurationListener to send signals to the activity to write data to the beacon.
+ */
+ public void saveChanges() {
+ if (txPowerChanged) {
+ int newRadioTxPower = Integer.parseInt(txPower);
+ configurationListener.txPowerChanged(slotNumber, newRadioTxPower);
+ txPowerChanged = false;
+ }
+
+ if (advTxPowerChanged) {
+ int newAdvTxPower = Integer.parseInt(advTxPower);
+ configurationListener.advTxPowerChanged(slotNumber, newAdvTxPower);
+ advTxPowerChanged = false;
+ }
+
+ if (advIntervalChanged) {
+ int newAdvInterval = Integer.parseInt(advInterval);
+ configurationListener.advIntervalChanged(slotNumber, newAdvInterval);
+ advIntervalChanged = false;
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ configurationListener = null;
+ }
+
+ public boolean changesPending() {
+ return advIntervalChanged || advTxPowerChanged || txPowerChanged;
+ }
+
+ public String getTxPower() {
+ return txPower;
+ }
+
+ public String getAdvTxPower() {
+ return advTxPower;
+ }
+
+ public String getAdvInterval() {
+ return advInterval;
+ }
+
+ public void setTxPower(int txPower) {
+ this.txPower = Integer.toString(txPower);
+ txPowerChanged = true;
+ }
+
+ public void setAdvTxPower(int advTxPower) {
+ this.advTxPower = Integer.toString(advTxPower);
+ advTxPowerChanged = true;
+ }
+
+ public void setAdvInterval(int advInterval) {
+ this.advInterval = Integer.toString(advInterval);
+ advIntervalChanged = true;
+ }
+
+ /**
+ * Used for communication between the configuration activity
+ * and all the fragments in it.
+ */
+ public interface ConfigurationListener {
+ void txPowerChanged(int slot, int txPower);
+ void advTxPowerChanged(int slot, int advTxPower);
+ void advIntervalChanged(int slot, int advInterval);
+ void slotDataChanged(int slot, byte[] slotData);
+ void lockCodeChanged(String newLockCode);
+ void factoryResetCalled();
+ void remainConnectableChanged(boolean remainConnectable);
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/layout/GlobalFragment.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/layout/GlobalFragment.java
new file mode 100644
index 0000000..03d434c
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/layout/GlobalFragment.java
@@ -0,0 +1,261 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package layout;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import android.widget.TextView;
+
+import com.github.google.beaconfig.Constants;
+import com.github.google.beaconfig.R;
+import com.github.google.beaconfig.dialogs.ChangePasswordDialog;
+import com.github.google.beaconfig.dialogs.ConfirmationDialog;
+
+/**
+ * First fragment in the tab layout in the Beacon Activity. It shows global information about the
+ * beacon and provides a way to set global configurations. If per slot radio tx power is not
+ * supported, it will show here. Same with advertising interval. Also factory reset, change of lock
+ * code and remain connectable configuration is set from here.
+ */
+public class GlobalFragment extends BeaconTabFragment {
+ private String beaconAddress;
+ private String beaconName;
+
+ private boolean isRemainConnectableSupported;
+
+ private boolean remainConnectableChanged;
+ private boolean remainConnectable;
+
+ public static GlobalFragment newInstance(Bundle bundle) {
+ GlobalFragment globalFragment = new GlobalFragment();
+ globalFragment.setArguments(bundle);
+ globalFragment.slotNumber = -1;
+ return globalFragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.fragment_frame_slot,
+ (LinearLayout) container.findViewById(R.id.global_content), false);
+
+ setUpFragment(v);
+ return v;
+ }
+
+ /**
+ * Updates slot information by pulling values from the bundle. These values can be:
+ * - beacon address type: String key: BEACON_ADDRESS
+ * - beacon name type: String key: BEACON_NAME
+ * - remainConnectable type: Byte key: REMAIN_CONNECTABLE
+ * - broadcast capabilities type: byte[] key: BROADCAST_CAPABILITIES
+ * - radio tx power type: String key: TX_POWER
+ * - adv tx power type: String key: ADV_POWER
+ * - adv interval type: String key: ADV_INTERVAL
+ *
+ * @param bundle bundle containing all the information to be updated
+ */
+ public void updateInformation(final Bundle bundle) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ GlobalFragment.super.updateInformation(bundle);
+
+ String beaconAddress = bundle.getString(Constants.BEACON_ADDRESS);
+ if (beaconAddress != null) {
+ GlobalFragment.this.beaconAddress = beaconAddress;
+ }
+
+ String beaconName = bundle.getString(Constants.BEACON_NAME);
+ if (beaconName != null) {
+ GlobalFragment.this.beaconName = beaconName;
+ }
+
+ Byte remainConnectable = bundle.getByte(Constants.REMAIN_CONNECTABLE);
+ if (remainConnectable != null) {
+ GlobalFragment.this.isRemainConnectableSupported = (remainConnectable != 0);
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void setUpFragment(final View v) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ GlobalFragment.super.setUpFragment(v);
+ setUpGlobalInformation(v);
+ setupRemainConnectable(v);
+ setUpLockCodeChange(v);
+ setUpFactoryReset(v);
+ }
+ });
+ }
+
+ /**
+ * Changing the lock code opens a dialog which asks to give new lock code and repeat it.
+ * When the user is done, the new lock code is written to the beacon.
+ *
+ * @param v container for the change lock code component. This is usually the main container in
+ * the tab. It has to be added to the activity before calling this method.
+ */
+ private void setUpLockCodeChange(View v) {
+ v.findViewById(R.id.lock_code_slot).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ChangePasswordDialog.show(getActivity(),
+ new ChangePasswordDialog.PasswordChangeListener() {
+ @Override
+ public void passwordChangeRequest(String newPassword) {
+ configurationListener.lockCodeChanged(newPassword);
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * This sets all the TextViews in the UI to show the information read from the beacon.
+ *
+ * @param v container for the global information component. This is usually the main container
+ * in the tab. It has to be added to the activity before calling this method.
+ */
+ private void setUpGlobalInformation(View v) {
+ v.findViewById(R.id.global_content).setVisibility(View.VISIBLE);
+ ((TextView) v.findViewById(R.id.beacon_address)).setText(beaconAddress);
+ ((TextView) v.findViewById(R.id.beacon_name)).setText(beaconName);
+ ((TextView) v.findViewById(R.id.max_eid_slots))
+ .setText(Integer.toString(capabilities.getMaxSupportedEidSlots()));
+ ((TextView) v.findViewById(R.id.max_total_slots))
+ .setText(Integer.toString(capabilities.getMaxSupportedTotalSlots()));
+ }
+
+ /**
+ * This asks for confirmation before factory resetting the beacon.
+ *
+ * @param v container for the factory reset component. This is usually the main container in
+ * the tab. It has to be added to the activity before calling this method.
+ */
+ private void setUpFactoryReset(View v) {
+ v.findViewById(R.id.factory_reset).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ ConfirmationDialog.confirm("Factory Reset", "Are you sure you want to factory "
+ + "reset this beacon? \n \n The beacon will be reset to its initial state"
+ + " set by its manufacturer. \n \n Its unlock code will NOT be changed.",
+ "RESET", "CANCEL", getActivity(),
+ new ConfirmationDialog.ConfirmationListener() {
+ @Override
+ public void confirm() {
+ configurationListener.factoryResetCalled();
+ }
+
+ @Override
+ public void cancel() {
+
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Remain connectable is controlled by a radio group of 2 buttons. Initially these buttons are
+ * hidden and on click the buttons show and remain connectable will be configured.
+ *
+ * @param v container for the remain connectable component. This is usually the main container
+ * in the tab. It has to be added to the activity before calling this method.
+ */
+ private void setupRemainConnectable(final View v) {
+ ViewGroup connectableSlot = (ViewGroup) v.findViewById(R.id.remain_connectable_slot);
+ if (isRemainConnectableSupported) {
+ connectableSlot.setVisibility(View.VISIBLE);
+
+ // Set expand and hide of radio group buttons on click of remain connectable field
+ connectableSlot.setOnClickListener(new View.OnClickListener() {
+ boolean expanded = false;
+ @Override
+ public void onClick(View view) {
+ if (expanded) {
+ v.findViewById(R.id.set_remain_connectable).setVisibility(View.GONE);
+ ((ImageView) v.findViewById(R.id.remain_connectable_icon))
+ .setImageResource(R.drawable.expand_arrow);
+ expanded = false;
+ } else {
+ v.findViewById(R.id.set_remain_connectable).setVisibility(View.VISIBLE);
+ ((ImageView) v.findViewById(R.id.remain_connectable_icon))
+ .setImageResource(R.drawable.close_arrow);
+ expanded = true;
+ }
+ }
+ });
+
+ // Set first radio button to configure turning ON of remain connectable
+ final RadioButton onButton = (RadioButton) v.findViewById(R.id.remain_connectable_on);
+ onButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+ remainConnectableChanged = true;
+ RadioButton offBtn = (RadioButton) v.findViewById(R.id.remain_connectable_off);
+ if (b) {
+ offBtn.setChecked(false);
+ onButton.setChecked(true);
+ remainConnectable = true;
+ }
+ }
+ });
+
+ // Set first radio button to configure turning OFF of remain connectable
+ final RadioButton offButton = (RadioButton) v.findViewById(R.id.remain_connectable_on);
+ offButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
+ remainConnectableChanged = true;
+ RadioButton onBtn = (RadioButton) v.findViewById(R.id.remain_connectable_off);
+ if (b) {
+ onBtn.setChecked(false);
+ offButton.setChecked(true);
+ remainConnectable = false;
+ }
+ }
+ });
+ } else {
+ connectableSlot.setVisibility(View.GONE);
+ }
+ }
+
+ @Override
+ public void saveChanges() {
+ super.saveChanges();
+
+ if (remainConnectableChanged) {
+ configurationListener.remainConnectableChanged(remainConnectable);
+ remainConnectableChanged = false;
+ }
+ }
+
+ @Override
+ public boolean changesPending() {
+ return super.changesPending() || remainConnectableChanged;
+ }
+
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/java/layout/SlotFragment.java b/tools/gatt-config/android/BeaconConfig/app/src/main/java/layout/SlotFragment.java
new file mode 100644
index 0000000..dfdfa83
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/java/layout/SlotFragment.java
@@ -0,0 +1,373 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+package layout;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.LinearLayout;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.github.google.beaconfig.Constants;
+import com.github.google.beaconfig.R;
+import com.github.google.beaconfig.dialogs.UidSlotDataChangeDialog;
+import com.github.google.beaconfig.dialogs.UrlChangeDialog;
+import com.github.google.beaconfig.utils.SlotDataManager;
+import com.github.google.beaconfig.utils.Utils;
+
+/**
+ * Fragment for displaying and configuring information of a frame slot in the beacon. This is a
+ * unifying fragment which can be used for a UID, URL, TLM or EID frame.
+ */
+public class SlotFragment extends BeaconTabFragment implements AdapterView.OnItemSelectedListener {
+ public static final String TAG = SlotFragment.class.getSimpleName();
+ private boolean slotDataChanged;
+
+ private byte currFrameType;
+ private byte[] slotData;
+
+ /* UID fields */
+ private String namespace;
+ private String instance;
+
+ /* URL field */
+ private String url;
+
+ /* TLM fields */
+ private String voltage;
+ private String temperature;
+ private String advCnt;
+ private String secCnt;
+
+ /* EID field */
+ private String ephemeralId;
+
+ public static SlotFragment newInstance(Bundle bundle) {
+ SlotFragment newSlotFragment = new SlotFragment();
+ newSlotFragment.slotNumber = bundle.getInt(Constants.SLOT_NUMBER);
+ newSlotFragment.setArguments(bundle);
+ newSlotFragment.name
+ = Utils.getFrameNameFromSlotData(bundle.getByteArray(Constants.SLOT_DATA));
+ return newSlotFragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ Log.d(TAG, "SlotFragment.onCreateView");
+ View v = inflater.inflate(R.layout.fragment_frame_slot,
+ (LinearLayout) container.findViewById(R.id.global_content), false);
+ v.findViewById(R.id.slot_information).setVisibility(View.VISIBLE);
+
+ setUpFrameTypeSpinner(v);
+ setUpFragment(v);
+ return v;
+ }
+
+ /**
+ * Setting up the spinner which configures the frame type of a beacon slot.
+ *
+ * @param v container for the spinner. This is usually the main container in
+ * the tab. It has to be added to the activity before calling this method.
+ */
+ private void setUpFrameTypeSpinner(View v) {
+ final Spinner frameTypeSpinner = (Spinner) v.findViewById(R.id.frame_type_spinner);
+ ArrayAdapter spinnerAdapter = ArrayAdapter.createFromResource(getContext(),
+ R.array.frame_types, android.R.layout.simple_spinner_item);
+ spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ frameTypeSpinner.setAdapter(spinnerAdapter);
+ frameTypeSpinner.setOnItemSelectedListener(this);
+ v.findViewById(R.id.frame_type_slot).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ frameTypeSpinner.performClick();
+ }
+ });
+ }
+
+ /**
+ * Reads information from the bundle according to which frame type this fragment is set to.
+ * The values which can be read from the bundle are:
+ * - slot data type: byte[] key: SLOT_DATA
+ * - broadcast capabilities type: byte[] key: BROADCAST_CAPABILITIES
+ * - radio tx power type: String key: TX_POWER
+ * - adv tx power type: String key: ADV_POWER
+ * - adv interval type: String key: ADV_INTERVAL
+ *
+ * @param dataBundle bundle containing all the information to be updated
+ */
+ public void updateInformation(final Bundle dataBundle) {
+ if (dataBundle == null) {
+ Log.d(TAG, "Updating information for slot " + slotNumber
+ + " failed due to empty bundle");
+ return;
+ }
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ SlotFragment.super.updateInformation(dataBundle);
+ byte[] slotData = dataBundle.getByteArray(Constants.SLOT_DATA);
+ if (slotData != null) {
+ SlotFragment.this.slotData = slotData;
+ if (Utils.slotIsEmpty(slotData)) {
+ currFrameType = Constants.EMPTY_FRAME_TYPE;
+ } else {
+ currFrameType = slotData[0];
+ switch (currFrameType) {
+ case Constants.UID_FRAME_TYPE:
+ name = Constants.UID;
+ namespace = SlotDataManager.getNamespaceFromSlotData(slotData);
+ instance = SlotDataManager.getInstanceFromSlotData(slotData);
+ break;
+ case Constants.URL_FRAME_TYPE:
+ name = Constants.URL;
+
+ url = SlotDataManager.getUrlFromSlotData(slotData);
+ break;
+ case Constants.TLM_FRAME_TYPE:
+ name = Constants.TLM;
+ voltage = Short.toString(SlotDataManager
+ .getVoltageFromSlotData(slotData));
+
+ temperature = Float.toString(SlotDataManager
+ .getTemperatureFromSlotData(slotData));
+
+ advCnt = Integer.toString(SlotDataManager
+ .getAdvertisingPDUCountFromSlotData(slotData));
+
+ secCnt = Utils.getTimeString(SlotDataManager
+ .getTimeSinceOnFromSlotData(slotData));
+ break;
+ case Constants.EID_FRAME_TYPE:
+ name = Constants.EID;
+
+ ephemeralId = SlotDataManager.getEphemeralIdFromSlotData(slotData);
+ break;
+ default:
+ name = "--";
+ }
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Sets up the fragment according to the frame type this slot is configured to broadcast
+ *
+ * @param v container for the tab. It has to be added to the activity before calling this method
+ */
+ protected void setUpFragment(final View v) {
+ if (getActivity() != null) {
+ getActivity().runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ hideAllViews(v);
+
+ switch (currFrameType) {
+ case Constants.UID_FRAME_TYPE:
+ setUpFragmentAsUidSlot(v);
+ break;
+ case Constants.URL_FRAME_TYPE:
+ setUpFragmentAsUrlSlot(v);
+ break;
+ case Constants.TLM_FRAME_TYPE:
+ setUpFragmentAsTlmSlot(v);
+ break;
+ case Constants.EID_FRAME_TYPE:
+ setUpFragmentAsEidSlot(v);
+ break;
+ default:
+ ((Spinner) v.findViewById(R.id.frame_type_spinner)).setSelection(4);
+ name = "--";
+ return;
+ }
+
+ SlotFragment.super.setUpFragment(v);
+ }
+ });
+ }
+ }
+
+ @Override
+ public boolean changesPending() {
+ return super.changesPending() || slotDataChanged;
+ }
+
+ /**
+ * @return true whenever the frame this slot is configured to is empty
+ */
+ public boolean isEmpty() {
+ return currFrameType == Constants.EMPTY_FRAME_TYPE;
+ }
+
+ private void hideAllViews(final View v) {
+ v.findViewById(R.id.uid_frame_slot).setVisibility(View.GONE);
+ v.findViewById(R.id.url_frame_slot).setVisibility(View.GONE);
+ v.findViewById(R.id.tlm_frame_slot).setVisibility(View.GONE);
+ v.findViewById(R.id.eid_frame_slot).setVisibility(View.GONE);
+ v.findViewById(R.id.tx_power_info).setVisibility(View.GONE);
+ v.findViewById(R.id.adv_int_info).setVisibility(View.GONE);
+ }
+
+ /**
+ * Called when a new entry in the frame type spinner is selected. This will trigger reloading
+ * of the fragment as new fields will need to be shown.
+ */
+ private boolean firstTime = true;
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int pos, long id) {
+ String newFrameSelected = (String) parent.getItemAtPosition(pos);
+ currFrameType = Utils.getFrameTypeFromString(newFrameSelected);
+ if (firstTime) {
+ firstTime = false;
+ } else {
+ slotDataChanged = true;
+ }
+ setUpFragment(getView());
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> adapterView) {
+ }
+
+ private void setUpFragmentAsEidSlot(View v) {
+ Log.d(TAG, "Setting tab " + slotNumber + " as EID slot");
+ ((Spinner) v.findViewById(R.id.frame_type_spinner)).setSelection(3);
+ v.findViewById(R.id.eid_frame_slot).setVisibility(View.VISIBLE);
+
+ ((TextView) v.findViewById(R.id.ephemeral_id)).setText(ephemeralId);
+ }
+
+ private void setUpFragmentAsTlmSlot(View v) {
+ Log.d(TAG, "Setting tab " + slotNumber + " as TLM slot");
+ ((Spinner) v.findViewById(R.id.frame_type_spinner)).setSelection(2);
+ v.findViewById(R.id.tlm_frame_slot).setVisibility(View.VISIBLE);
+
+ ((TextView) v.findViewById(R.id.voltage)).setText(voltage);
+ ((TextView) v.findViewById(R.id.temperature)).setText(temperature);
+ ((TextView) v.findViewById(R.id.adv_pdu_count)).setText(advCnt);
+ ((TextView) v.findViewById(R.id.time_since_alive)).setText(secCnt);
+ }
+
+ private void setUpFragmentAsUrlSlot(View v) {
+ Log.d(TAG, "Setting tab " + slotNumber + " as URL slot");
+ ((Spinner) v.findViewById(R.id.frame_type_spinner)).setSelection(1);
+ final ViewGroup urlSlot = (ViewGroup) v.findViewById(R.id.url_frame_slot);
+ urlSlot.setVisibility(View.VISIBLE);
+
+ final TextView urlView = (TextView) urlSlot.findViewById(R.id.url);
+ urlView.setText(url);
+
+
+ urlSlot.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ UrlChangeDialog.show(getContext(), new UrlChangeDialog.UrlChangeListener() {
+ @Override
+ public void setNewUrl(String newUrl) {
+ urlView.setText(newUrl);
+ url = newUrl;
+ slotDataChanged = true;
+ }
+ });
+ }
+ });
+ }
+
+ private void setUpFragmentAsUidSlot(View v) {
+ Log.d(TAG, "Setting tab " + slotNumber + " as UID slot");
+ ((Spinner) v.findViewById(R.id.frame_type_spinner)).setSelection(0);
+ ViewGroup uidFrameSlot = (ViewGroup) v.findViewById(R.id.uid_frame_slot);
+ uidFrameSlot.setVisibility(View.VISIBLE);
+
+ final TextView namespaceView = (TextView) v.findViewById(R.id.namespace);
+ namespaceView.setText(namespace);
+
+ final TextView instanceView = (TextView) v.findViewById(R.id.instance);
+ instanceView.setText(instance);
+
+ uidFrameSlot.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ slotDataChanged = true;
+ UidSlotDataChangeDialog.show(namespace, instance, getContext(),
+ new UidSlotDataChangeDialog.UidChangeListener() {
+ @Override
+ public void setNewUid(String namespace, String instance) {
+ SlotFragment.this.namespace = namespace;
+ SlotFragment.this.instance = instance;
+ namespaceView.setText(namespace);
+ instanceView.setText(instance);
+ }
+ });
+ }
+ });
+
+
+ }
+
+ @Override
+ public void saveChanges() {
+ super.saveChanges();
+
+ if (slotDataChanged) {
+ slotData = buildNewSlotDataInfo();
+ configurationListener.slotDataChanged(slotNumber, slotData);
+ slotDataChanged = false;
+ }
+
+ setUpFragment(getView());
+ }
+
+ /**
+ * Whenever the slot data has been changed and the user wishes to save this change, a new
+ * byte[] will have to be written to the beacon. This method uses the SlotDataManager to
+ * build the byte[] according to the current frame the spinner is set to.
+ *
+ * @return byte[] to be written to the beacon to configure new slot data information
+ */
+ public byte[] buildNewSlotDataInfo() {
+ byte[] newSlotData;
+ switch(currFrameType) {
+ case Constants.UID_FRAME_TYPE:
+ String namespace
+ = ((TextView) getView().findViewById(R.id.namespace)).getText().toString();
+ String instance
+ = ((TextView) getView().findViewById(R.id.instance)).getText().toString();
+ newSlotData = SlotDataManager.buildNewUidSlotData(namespace, instance);
+ break;
+ case Constants.URL_FRAME_TYPE:
+ String newUrl = ((TextView) getView().findViewById(R.id.url)).getText().toString();
+ newSlotData = SlotDataManager.buildNewUrlSlotData(newUrl);
+ break;
+ case Constants.TLM_FRAME_TYPE:
+ newSlotData = SlotDataManager.buildNewTlmSlotData();
+ break;
+ case Constants.EID_FRAME_TYPE:
+ newSlotData = SlotDataManager.buildNewEidSlotData();
+ break;
+ default:
+ //empty slot
+ newSlotData = new byte[0];
+ }
+ return newSlotData;
+ }
+}
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/apply_default_config.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/apply_default_config.xml
new file mode 100644
index 0000000..8031fd5
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/apply_default_config.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/arrow_right.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/arrow_right.xml
new file mode 100644
index 0000000..51f880f
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/arrow_right.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/circle.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/circle.xml
new file mode 100644
index 0000000..680dcaf
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/circle.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/close_arrow.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/close_arrow.xml
new file mode 100644
index 0000000..9b799d7
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/close_arrow.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/cross.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/cross.xml
new file mode 100644
index 0000000..c820415
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/cross.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/delete_grey.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/delete_grey.xml
new file mode 100644
index 0000000..cfd6c49
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/delete_grey.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/expand_arrow.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/expand_arrow.xml
new file mode 100644
index 0000000..4b5c22a
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/expand_arrow.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/factory_reset_icon.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/factory_reset_icon.xml
new file mode 100644
index 0000000..b34d539
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/factory_reset_icon.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/globe.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/globe.xml
new file mode 100644
index 0000000..f596e49
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/globe.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/pencil.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/pencil.xml
new file mode 100644
index 0000000..63c8944
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/pencil.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/refresh.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/refresh.xml
new file mode 100644
index 0000000..5c15f62
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/refresh.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/save_changes.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/save_changes.xml
new file mode 100644
index 0000000..6b251ff
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/save_changes.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/save_config.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/save_config.xml
new file mode 100644
index 0000000..fe53a44
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/save_config.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/shadow.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/shadow.xml
new file mode 100644
index 0000000..6fcaf53
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/shadow.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/side_nav_bar.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/side_nav_bar.xml
new file mode 100644
index 0000000..458b4b0
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/drawable/side_nav_bar.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/activity_beacon_config.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/activity_beacon_config.xml
new file mode 100644
index 0000000..7606912
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/activity_beacon_config.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/activity_main.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..70cbfb5
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/activity_scanning.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/activity_scanning.xml
new file mode 100644
index 0000000..b02cd02
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/activity_scanning.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/content_scanning.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/content_scanning.xml
new file mode 100644
index 0000000..a479e9e
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/content_scanning.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_change_lock_code.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_change_lock_code.xml
new file mode 100644
index 0000000..3cd9879
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_change_lock_code.xml
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_change_uid.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_change_uid.xml
new file mode 100644
index 0000000..4ce0d37
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_change_uid.xml
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_change_url.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_change_url.xml
new file mode 100644
index 0000000..e13d4fb
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_change_url.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_save_config.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_save_config.xml
new file mode 100644
index 0000000..bb56e62
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_save_config.xml
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_unlock.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_unlock.xml
new file mode 100644
index 0000000..4b759fe
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/dialog_unlock.xml
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/fragment_frame_slot.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/fragment_frame_slot.xml
new file mode 100644
index 0000000..27af412
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/fragment_frame_slot.xml
@@ -0,0 +1,668 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/fragment_manage_configurations.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/fragment_manage_configurations.xml
new file mode 100644
index 0000000..32c7e74
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/fragment_manage_configurations.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/fragment_manage_configurations_item.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/fragment_manage_configurations_item.xml
new file mode 100644
index 0000000..7f648df
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/fragment_manage_configurations_item.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/frame_row_tlm.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/frame_row_tlm.xml
new file mode 100644
index 0000000..d8e79f9
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/frame_row_tlm.xml
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/frame_row_uid.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/frame_row_uid.xml
new file mode 100644
index 0000000..27c15d2
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/frame_row_uid.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/frame_row_url.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/frame_row_url.xml
new file mode 100644
index 0000000..70ba1a4
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/frame_row_url.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+ />
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/frame_type_row.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/frame_type_row.xml
new file mode 100644
index 0000000..b82fcdf
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/frame_type_row.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/saved_slot_configuration.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/saved_slot_configuration.xml
new file mode 100644
index 0000000..124562e
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/saved_slot_configuration.xml
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/scan_result_layout.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/scan_result_layout.xml
new file mode 100644
index 0000000..98c3ed8
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/scan_result_layout.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/spinner_item.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/spinner_item.xml
new file mode 100644
index 0000000..bd46908
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/spinner_item.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/tabs_layout.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/tabs_layout.xml
new file mode 100644
index 0000000..1c91b62
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/layout/tabs_layout.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/menu/menu_beacon_config.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/menu/menu_beacon_config.xml
new file mode 100644
index 0000000..d9a71fe
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/menu/menu_beacon_config.xml
@@ -0,0 +1,26 @@
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-hdpi/ic_launcher.png b/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..420f814
Binary files /dev/null and b/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-mdpi/ic_launcher.png b/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..1ea07b3
Binary files /dev/null and b/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..eafdb37
Binary files /dev/null and b/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..10033e2
Binary files /dev/null and b/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..a3422f8
Binary files /dev/null and b/tools/gatt-config/android/BeaconConfig/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/values-v21/styles.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/values-v21/styles.xml
new file mode 100644
index 0000000..dbbdd40
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/values-v21/styles.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/values-w820dp/dimens.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/values-w820dp/dimens.xml
new file mode 100644
index 0000000..63fc816
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/values-w820dp/dimens.xml
@@ -0,0 +1,6 @@
+
+
+ 64dp
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/values/colors.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..ac55bef
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/values/colors.xml
@@ -0,0 +1,11 @@
+
+
+ #A4C639
+ #A4C639
+ #A4C639
+ #000000
+ #d92020
+ #c4c4c4
+ #ffffff
+ #f1707070
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/values/dimens.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..18d977c
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/values/dimens.xml
@@ -0,0 +1,19 @@
+
+
+ 16dp
+ 16dp
+ 16dp
+ 8dp
+ 10dp
+ 40dp
+ 1dp
+ 30dp
+ 20dp
+ 15dp
+ 12dp
+ 10dp
+
+ 16dp
+ 160dp
+ 16dp
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/values/strings.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..dbfa43f
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/values/strings.xml
@@ -0,0 +1,31 @@
+
+ Beaconfig
+ Settings
+ BeaconConfig
+ Save Changes
+ Hello World from section: %1$d
+
+
+ Hello blank fragment
+
+ UID
+ URL
+ TLM
+ EID
+ [empty]
+
+
+ https://www.
+ http://www.
+ https://
+ http://
+
+ Changes saved successfully
+ Make sure that bluetooth is turned on and beacon
+ is connectable. \n \n You might have to press a button or shake the beacon to make
+ it connectable. \n \n
+
+ Open navigation drawer
+ Close navigation drawer
+ MainActivity
+
diff --git a/tools/gatt-config/android/BeaconConfig/app/src/main/res/values/styles.xml b/tools/gatt-config/android/BeaconConfig/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..5e3ea21
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/app/src/main/res/values/styles.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/gatt-config/android/BeaconConfig/build.gradle b/tools/gatt-config/android/BeaconConfig/build.gradle
new file mode 100644
index 0000000..aff4f41
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/build.gradle
@@ -0,0 +1,23 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.1.2'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/tools/gatt-config/android/BeaconConfig/gradle.properties b/tools/gatt-config/android/BeaconConfig/gradle.properties
new file mode 100644
index 0000000..1d3591c
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/gradle.properties
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
\ No newline at end of file
diff --git a/tools/gatt-config/android/BeaconConfig/gradle/wrapper/gradle-wrapper.jar b/tools/gatt-config/android/BeaconConfig/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..13372ae
Binary files /dev/null and b/tools/gatt-config/android/BeaconConfig/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/tools/gatt-config/android/BeaconConfig/gradle/wrapper/gradle-wrapper.properties b/tools/gatt-config/android/BeaconConfig/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..122a0dc
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Dec 28 10:00:20 PST 2015
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
diff --git a/tools/gatt-config/android/BeaconConfig/gradlew b/tools/gatt-config/android/BeaconConfig/gradlew
new file mode 100755
index 0000000..9d82f78
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/tools/gatt-config/android/BeaconConfig/gradlew.bat b/tools/gatt-config/android/BeaconConfig/gradlew.bat
new file mode 100644
index 0000000..8a0b282
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/tools/gatt-config/android/BeaconConfig/settings.gradle b/tools/gatt-config/android/BeaconConfig/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/tools/gatt-config/android/BeaconConfig/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/tools/gatt-config/android/README.md b/tools/gatt-config/android/README.md
new file mode 100644
index 0000000..203163b
--- /dev/null
+++ b/tools/gatt-config/android/README.md
@@ -0,0 +1 @@
+TODO: describe this app, how to build and run, etc.
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig.xcodeproj/project.pbxproj b/tools/gatt-config/ios/Beaconfig/Beaconfig.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..7173e58
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig.xcodeproj/project.pbxproj
@@ -0,0 +1,487 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 2B20915F1D95293800D65C32 /* README.md in Sources */ = {isa = PBXBuildFile; fileRef = 2B20915E1D95293800D65C32 /* README.md */; };
+ 2B2A54861D771CC6000D6A44 /* UserLoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2A54851D771CC6000D6A44 /* UserLoginViewController.swift */; };
+ 2B30DB111D5B268900C206AE /* StringUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B30DB101D5B268900C206AE /* StringUtils.swift */; };
+ 2B30DB171D5B57C000C206AE /* HorizontalScrollButtonList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B30DB161D5B57C000C206AE /* HorizontalScrollButtonList.swift */; };
+ 2B31ECEA1D89BCEB00E15683 /* GoogleService-Info.plist.sample in Resources */ = {isa = PBXBuildFile; fileRef = 2B31ECE91D89BCEB00E15683 /* GoogleService-Info.plist.sample */; };
+ 2B4621661D50B2BE00C21672 /* BeaconInvestigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B4621651D50B2BE00C21672 /* BeaconInvestigation.swift */; };
+ 2B9241521D3F7F210038628A /* BeaconTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9241511D3F7F210038628A /* BeaconTableViewCell.swift */; };
+ 2B92878B1D3FD60800C10F76 /* BeaconFramesSupported.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B92878A1D3FD60800C10F76 /* BeaconFramesSupported.swift */; };
+ 2B9B24611D6DDE38000B8EA0 /* CustomViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9B24601D6DDE38000B8EA0 /* CustomViews.swift */; };
+ 2B9B24631D6DE054000B8EA0 /* BeaconItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9B24621D6DE054000B8EA0 /* BeaconItem.swift */; };
+ 2BA0354F1D5DD10C00282F58 /* ConfigureBeaconSlotData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BA0354E1D5DD10C00282F58 /* ConfigureBeaconSlotData.swift */; };
+ 2BA4C43A1D3D2D890050395B /* ScannedBeaconsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BA4C4391D3D2D890050395B /* ScannedBeaconsTableViewController.swift */; };
+ 2BB5C9401D3CE53000A697C9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB5C93F1D3CE53000A697C9 /* AppDelegate.swift */; };
+ 2BB5C9451D3CE53000A697C9 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2BB5C9431D3CE53000A697C9 /* Main.storyboard */; };
+ 2BB5C9481D3CE53000A697C9 /* Beaconfig.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 2BB5C9461D3CE53000A697C9 /* Beaconfig.xcdatamodeld */; };
+ 2BB5C94A1D3CE53000A697C9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2BB5C9491D3CE53000A697C9 /* Assets.xcassets */; };
+ 2BB5C94D1D3CE53000A697C9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2BB5C94B1D3CE53000A697C9 /* LaunchScreen.storyboard */; };
+ 2BB5C9551D3CECDA00A697C9 /* BeaconScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB5C9541D3CECDA00A697C9 /* BeaconScanner.swift */; };
+ 2BB5C9571D3CED5300A697C9 /* Eddystone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BB5C9561D3CED5300A697C9 /* Eddystone.swift */; };
+ 2BC215AE1D75E61B007AD41D /* EIDConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BC215AD1D75E61B007AD41D /* EIDConfiguration.swift */; };
+ 2BCE59FF1D54D6D20066997F /* SlotDataContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BCE59FE1D54D6D20066997F /* SlotDataContentView.swift */; };
+ 2BD320551D805B760026CB22 /* UserInformation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BD320541D805B760026CB22 /* UserInformation.swift */; };
+ 2BD320571D81BED90026CB22 /* HTTPRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BD320561D81BED90026CB22 /* HTTPRequest.swift */; };
+ 2BD3205D1D8842050026CB22 /* ConnectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BD3205C1D8842050026CB22 /* ConnectionView.swift */; };
+ 2BED28371D412278008DD2D6 /* BeaconDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BED28361D412278008DD2D6 /* BeaconDetailsViewController.swift */; };
+ 2BED28391D412525008DD2D6 /* GATTOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BED28381D412525008DD2D6 /* GATTOperations.swift */; };
+ 4E226EDD9FD9033C1E87DAEC /* Pods_Beaconfig.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DFEA709B7DC8FFBA6D3939D7 /* Pods_Beaconfig.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 2B20915E1D95293800D65C32 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
+ 2B2A54851D771CC6000D6A44 /* UserLoginViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserLoginViewController.swift; sourceTree = ""; };
+ 2B30DB101D5B268900C206AE /* StringUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringUtils.swift; sourceTree = ""; };
+ 2B30DB161D5B57C000C206AE /* HorizontalScrollButtonList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalScrollButtonList.swift; sourceTree = ""; };
+ 2B31ECE91D89BCEB00E15683 /* GoogleService-Info.plist.sample */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "GoogleService-Info.plist.sample"; sourceTree = ""; };
+ 2B4621651D50B2BE00C21672 /* BeaconInvestigation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BeaconInvestigation.swift; sourceTree = ""; };
+ 2B9241511D3F7F210038628A /* BeaconTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BeaconTableViewCell.swift; sourceTree = ""; };
+ 2B92878A1D3FD60800C10F76 /* BeaconFramesSupported.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BeaconFramesSupported.swift; sourceTree = ""; };
+ 2B9B24601D6DDE38000B8EA0 /* CustomViews.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomViews.swift; sourceTree = ""; };
+ 2B9B24621D6DE054000B8EA0 /* BeaconItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BeaconItem.swift; sourceTree = ""; };
+ 2BA0354E1D5DD10C00282F58 /* ConfigureBeaconSlotData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigureBeaconSlotData.swift; sourceTree = ""; };
+ 2BA4C4391D3D2D890050395B /* ScannedBeaconsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScannedBeaconsTableViewController.swift; sourceTree = ""; };
+ 2BB5C93C1D3CE53000A697C9 /* Beaconfig.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Beaconfig.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 2BB5C93F1D3CE53000A697C9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 2BB5C9441D3CE53000A697C9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 2BB5C9471D3CE53000A697C9 /* Beaconfig.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Beaconfig.xcdatamodel; sourceTree = ""; };
+ 2BB5C9491D3CE53000A697C9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 2BB5C94C1D3CE53000A697C9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 2BB5C94E1D3CE53000A697C9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 2BB5C9541D3CECDA00A697C9 /* BeaconScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BeaconScanner.swift; sourceTree = ""; };
+ 2BB5C9561D3CED5300A697C9 /* Eddystone.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Eddystone.swift; sourceTree = ""; };
+ 2BC215AD1D75E61B007AD41D /* EIDConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EIDConfiguration.swift; sourceTree = ""; };
+ 2BCE59FE1D54D6D20066997F /* SlotDataContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlotDataContentView.swift; sourceTree = ""; };
+ 2BD320541D805B760026CB22 /* UserInformation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserInformation.swift; sourceTree = ""; };
+ 2BD320561D81BED90026CB22 /* HTTPRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPRequest.swift; sourceTree = ""; };
+ 2BD3205C1D8842050026CB22 /* ConnectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionView.swift; sourceTree = ""; };
+ 2BED28361D412278008DD2D6 /* BeaconDetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BeaconDetailsViewController.swift; sourceTree = ""; };
+ 2BED28381D412525008DD2D6 /* GATTOperations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GATTOperations.swift; sourceTree = ""; };
+ 2BFE187D1D4649AD008E0CEA /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; };
+ A47CDA43C4C36E75273D3548 /* Pods-Beaconfig.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Beaconfig.release.xcconfig"; path = "Pods/Target Support Files/Pods-Beaconfig/Pods-Beaconfig.release.xcconfig"; sourceTree = ""; };
+ CE105E4B9E0A2BC378EF351C /* Pods-Beaconfig.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Beaconfig.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Beaconfig/Pods-Beaconfig.debug.xcconfig"; sourceTree = ""; };
+ DFEA709B7DC8FFBA6D3939D7 /* Pods_Beaconfig.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Beaconfig.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 2BB5C9391D3CE53000A697C9 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 4E226EDD9FD9033C1E87DAEC /* Pods_Beaconfig.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 2BB5C9331D3CE53000A697C9 = {
+ isa = PBXGroup;
+ children = (
+ 2B31ECE91D89BCEB00E15683 /* GoogleService-Info.plist.sample */,
+ 2BB5C93E1D3CE53000A697C9 /* Beaconfig */,
+ 2BB5C93D1D3CE53000A697C9 /* Products */,
+ A039EB4D606909EB98681B19 /* Pods */,
+ F019FD93CD2258602E221E24 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 2BB5C93D1D3CE53000A697C9 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 2BB5C93C1D3CE53000A697C9 /* Beaconfig.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 2BB5C93E1D3CE53000A697C9 /* Beaconfig */ = {
+ isa = PBXGroup;
+ children = (
+ 2B30DB161D5B57C000C206AE /* HorizontalScrollButtonList.swift */,
+ 2B9241511D3F7F210038628A /* BeaconTableViewCell.swift */,
+ 2BB5C93F1D3CE53000A697C9 /* AppDelegate.swift */,
+ 2BC215AD1D75E61B007AD41D /* EIDConfiguration.swift */,
+ 2BB5C9541D3CECDA00A697C9 /* BeaconScanner.swift */,
+ 2BD320561D81BED90026CB22 /* HTTPRequest.swift */,
+ 2B2A54851D771CC6000D6A44 /* UserLoginViewController.swift */,
+ 2B9B24621D6DE054000B8EA0 /* BeaconItem.swift */,
+ 2B9B24601D6DDE38000B8EA0 /* CustomViews.swift */,
+ 2BB5C9561D3CED5300A697C9 /* Eddystone.swift */,
+ 2B92878A1D3FD60800C10F76 /* BeaconFramesSupported.swift */,
+ 2BA4C4391D3D2D890050395B /* ScannedBeaconsTableViewController.swift */,
+ 2BED28361D412278008DD2D6 /* BeaconDetailsViewController.swift */,
+ 2BA0354E1D5DD10C00282F58 /* ConfigureBeaconSlotData.swift */,
+ 2BCE59FE1D54D6D20066997F /* SlotDataContentView.swift */,
+ 2BD3205C1D8842050026CB22 /* ConnectionView.swift */,
+ 2B30DB101D5B268900C206AE /* StringUtils.swift */,
+ 2BED28381D412525008DD2D6 /* GATTOperations.swift */,
+ 2B4621651D50B2BE00C21672 /* BeaconInvestigation.swift */,
+ 2BB5C9431D3CE53000A697C9 /* Main.storyboard */,
+ 2BD320541D805B760026CB22 /* UserInformation.swift */,
+ 2BB5C9491D3CE53000A697C9 /* Assets.xcassets */,
+ 2BB5C94B1D3CE53000A697C9 /* LaunchScreen.storyboard */,
+ 2BFE187D1D4649AD008E0CEA /* BridgingHeader.h */,
+ 2BB5C94E1D3CE53000A697C9 /* Info.plist */,
+ 2BB5C9461D3CE53000A697C9 /* Beaconfig.xcdatamodeld */,
+ 2B20915E1D95293800D65C32 /* README.md */,
+ );
+ path = Beaconfig;
+ sourceTree = "";
+ };
+ A039EB4D606909EB98681B19 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ CE105E4B9E0A2BC378EF351C /* Pods-Beaconfig.debug.xcconfig */,
+ A47CDA43C4C36E75273D3548 /* Pods-Beaconfig.release.xcconfig */,
+ );
+ name = Pods;
+ sourceTree = "";
+ };
+ F019FD93CD2258602E221E24 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ DFEA709B7DC8FFBA6D3939D7 /* Pods_Beaconfig.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 2BB5C93B1D3CE53000A697C9 /* Beaconfig */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 2BB5C9511D3CE53000A697C9 /* Build configuration list for PBXNativeTarget "Beaconfig" */;
+ buildPhases = (
+ A6D36E02376F7C579B05D31A /* [CP] Check Pods Manifest.lock */,
+ 2BB5C9381D3CE53000A697C9 /* Sources */,
+ 2BB5C9391D3CE53000A697C9 /* Frameworks */,
+ 2BB5C93A1D3CE53000A697C9 /* Resources */,
+ EBE4E81481A33BF54C3F0C5D /* [CP] Embed Pods Frameworks */,
+ 3B7ECC2B17A5D019859527D2 /* [CP] Copy Pods Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Beaconfig;
+ productName = Beaconfig;
+ productReference = 2BB5C93C1D3CE53000A697C9 /* Beaconfig.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 2BB5C9341D3CE53000A697C9 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastSwiftUpdateCheck = 0730;
+ LastUpgradeCheck = 0730;
+ ORGANIZATIONNAME = denisasandu;
+ TargetAttributes = {
+ 2BB5C93B1D3CE53000A697C9 = {
+ CreatedOnToolsVersion = 7.3.1;
+ DevelopmentTeam = 9F564R5U82;
+ };
+ };
+ };
+ buildConfigurationList = 2BB5C9371D3CE53000A697C9 /* Build configuration list for PBXProject "Beaconfig" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 2BB5C9331D3CE53000A697C9;
+ productRefGroup = 2BB5C93D1D3CE53000A697C9 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 2BB5C93B1D3CE53000A697C9 /* Beaconfig */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 2BB5C93A1D3CE53000A697C9 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 2BB5C94D1D3CE53000A697C9 /* LaunchScreen.storyboard in Resources */,
+ 2BB5C94A1D3CE53000A697C9 /* Assets.xcassets in Resources */,
+ 2B31ECEA1D89BCEB00E15683 /* GoogleService-Info.plist.sample in Resources */,
+ 2BB5C9451D3CE53000A697C9 /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3B7ECC2B17A5D019859527D2 /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Copy Pods Resources";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Beaconfig/Pods-Beaconfig-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ A6D36E02376F7C579B05D31A /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n";
+ showEnvVarsInLog = 0;
+ };
+ EBE4E81481A33BF54C3F0C5D /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Beaconfig/Pods-Beaconfig-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 2BB5C9381D3CE53000A697C9 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 2B30DB111D5B268900C206AE /* StringUtils.swift in Sources */,
+ 2BA4C43A1D3D2D890050395B /* ScannedBeaconsTableViewController.swift in Sources */,
+ 2B9B24631D6DE054000B8EA0 /* BeaconItem.swift in Sources */,
+ 2B9241521D3F7F210038628A /* BeaconTableViewCell.swift in Sources */,
+ 2BD3205D1D8842050026CB22 /* ConnectionView.swift in Sources */,
+ 2BCE59FF1D54D6D20066997F /* SlotDataContentView.swift in Sources */,
+ 2BB5C9571D3CED5300A697C9 /* Eddystone.swift in Sources */,
+ 2BED28371D412278008DD2D6 /* BeaconDetailsViewController.swift in Sources */,
+ 2BA0354F1D5DD10C00282F58 /* ConfigureBeaconSlotData.swift in Sources */,
+ 2BB5C9551D3CECDA00A697C9 /* BeaconScanner.swift in Sources */,
+ 2BB5C9481D3CE53000A697C9 /* Beaconfig.xcdatamodeld in Sources */,
+ 2BD320551D805B760026CB22 /* UserInformation.swift in Sources */,
+ 2B4621661D50B2BE00C21672 /* BeaconInvestigation.swift in Sources */,
+ 2BB5C9401D3CE53000A697C9 /* AppDelegate.swift in Sources */,
+ 2BED28391D412525008DD2D6 /* GATTOperations.swift in Sources */,
+ 2BC215AE1D75E61B007AD41D /* EIDConfiguration.swift in Sources */,
+ 2B9B24611D6DDE38000B8EA0 /* CustomViews.swift in Sources */,
+ 2B30DB171D5B57C000C206AE /* HorizontalScrollButtonList.swift in Sources */,
+ 2BD320571D81BED90026CB22 /* HTTPRequest.swift in Sources */,
+ 2B2A54861D771CC6000D6A44 /* UserLoginViewController.swift in Sources */,
+ 2B20915F1D95293800D65C32 /* README.md in Sources */,
+ 2B92878B1D3FD60800C10F76 /* BeaconFramesSupported.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 2BB5C9431D3CE53000A697C9 /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 2BB5C9441D3CE53000A697C9 /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 2BB5C94B1D3CE53000A697C9 /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 2BB5C94C1D3CE53000A697C9 /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 2BB5C94F1D3CE53000A697C9 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_IDENTITY = "iPhone Developer: denisasandu@google.com (3BZGMGDGL2)";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.3;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ SWIFT_OBJC_BRIDGING_HEADER = Beaconfig/BridgingHeader.h;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ };
+ name = Debug;
+ };
+ 2BB5C9501D3CE53000A697C9 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_IDENTITY = "iPhone Developer: denisasandu@google.com (3BZGMGDGL2)";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 9.3;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SWIFT_OBJC_BRIDGING_HEADER = Beaconfig/BridgingHeader.h;
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 2BB5C9521D3CE53000A697C9 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = CE105E4B9E0A2BC378EF351C /* Pods-Beaconfig.debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ INFOPLIST_FILE = Beaconfig/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = "denisansadu-google.com.beaconfig.application";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE = "";
+ SWIFT_OBJC_BRIDGING_HEADER = Beaconfig/BridgingHeader.h;
+ "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Beaconfig/BridgingHeader.h;
+ SWIFT_VERSION = 2.3;
+ };
+ name = Debug;
+ };
+ 2BB5C9531D3CE53000A697C9 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = A47CDA43C4C36E75273D3548 /* Pods-Beaconfig.release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ INFOPLIST_FILE = Beaconfig/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = "denisansadu-google.com.beaconfig.application";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE = "";
+ SWIFT_OBJC_BRIDGING_HEADER = Beaconfig/BridgingHeader.h;
+ "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Beaconfig/BridgingHeader.h;
+ SWIFT_VERSION = 2.3;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 2BB5C9371D3CE53000A697C9 /* Build configuration list for PBXProject "Beaconfig" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 2BB5C94F1D3CE53000A697C9 /* Debug */,
+ 2BB5C9501D3CE53000A697C9 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 2BB5C9511D3CE53000A697C9 /* Build configuration list for PBXNativeTarget "Beaconfig" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 2BB5C9521D3CE53000A697C9 /* Debug */,
+ 2BB5C9531D3CE53000A697C9 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+
+/* Begin XCVersionGroup section */
+ 2BB5C9461D3CE53000A697C9 /* Beaconfig.xcdatamodeld */ = {
+ isa = XCVersionGroup;
+ children = (
+ 2BB5C9471D3CE53000A697C9 /* Beaconfig.xcdatamodel */,
+ );
+ currentVersion = 2BB5C9471D3CE53000A697C9 /* Beaconfig.xcdatamodel */;
+ path = Beaconfig.xcdatamodeld;
+ sourceTree = "";
+ versionGroupType = wrapper.xcdatamodel;
+ };
+/* End XCVersionGroup section */
+ };
+ rootObject = 2BB5C9341D3CE53000A697C9 /* Project object */;
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/tools/gatt-config/ios/Beaconfig/Beaconfig.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..b750957
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/AppDelegate.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/AppDelegate.swift
new file mode 100644
index 0000000..1b59270
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/AppDelegate.swift
@@ -0,0 +1,64 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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 UIKit
+
+let kImageDimension: UInt = 180
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate, GIDSignInDelegate {
+
+ var window: UIWindow?
+
+ func application(application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
+ var configureError: NSError?
+ GGLContext.sharedInstance().configureWithError(&configureError)
+ assert(configureError == nil, "Error configuring Google services: \(configureError)")
+
+ GIDSignIn.sharedInstance().delegate = self
+ return true
+ }
+
+ func application(application: UIApplication,
+ openURL url: NSURL,
+ options: [String: AnyObject]) -> Bool {
+ return GIDSignIn.sharedInstance().handleURL(url,
+ sourceApplication: options[UIApplicationOpenURLOptionsSourceApplicationKey] as? String,
+ annotation: options[UIApplicationOpenURLOptionsAnnotationKey])
+ }
+
+ func signIn(signIn: GIDSignIn!, didSignInForUser user: GIDGoogleUser!,
+ withError error: NSError!) {
+ if (error == nil) {
+ let fullName = user.profile.name
+ var picture: NSURL?
+ if user.profile.hasImage {
+ picture = user.profile.imageURLWithDimension(kImageDimension)
+ }
+ let email = user.profile.email
+ NSNotificationCenter.defaultCenter().postNotificationName(
+ "ToggleAuthUINotification",
+ object: nil,
+ userInfo: ["statusText": "\(fullName)",
+ "email": email,
+ "imageURL": picture!])
+ } else {
+ print("\(error.localizedDescription)")
+ NSNotificationCenter.defaultCenter().postNotificationName(
+ "ToggleAuthUINotification", object: nil, userInfo: nil)
+ }
+ }
+}
+
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Contents.json b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..60a0f49
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,56 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..0601de8
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..50af655
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..542f067
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..92da810
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..2416f92
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..2878e91
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..2878e91
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..f3bf120
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/Contents.json b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..da4a164
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/Google.imageset/Contents.json b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/Google.imageset/Contents.json
new file mode 100644
index 0000000..23c26bb
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/Google.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "G.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/Google.imageset/G.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/Google.imageset/G.png
new file mode 100644
index 0000000..044e9b0
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/Google.imageset/G.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/beaconfig.imageset/Contents.json b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/beaconfig.imageset/Contents.json
new file mode 100644
index 0000000..1dfa415
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/beaconfig.imageset/Contents.json
@@ -0,0 +1,22 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_launcher-1.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_launcher.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/beaconfig.imageset/ic_launcher-1.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/beaconfig.imageset/ic_launcher-1.png
new file mode 100644
index 0000000..a3422f8
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/beaconfig.imageset/ic_launcher-1.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/beaconfig.imageset/ic_launcher.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/beaconfig.imageset/ic_launcher.png
new file mode 100644
index 0000000..a3422f8
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/beaconfig.imageset/ic_launcher.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/bluetoothConnection.imageset/Contents.json b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/bluetoothConnection.imageset/Contents.json
new file mode 100644
index 0000000..7f7aa4f
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/bluetoothConnection.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "ic_bluetooth_connected.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_bluetooth_connected_2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_bluetooth_connected_3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/bluetoothConnection.imageset/ic_bluetooth_connected.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/bluetoothConnection.imageset/ic_bluetooth_connected.png
new file mode 100644
index 0000000..8b093de
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/bluetoothConnection.imageset/ic_bluetooth_connected.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/bluetoothConnection.imageset/ic_bluetooth_connected_2x.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/bluetoothConnection.imageset/ic_bluetooth_connected_2x.png
new file mode 100644
index 0000000..2e2c409
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/bluetoothConnection.imageset/ic_bluetooth_connected_2x.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/bluetoothConnection.imageset/ic_bluetooth_connected_3x.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/bluetoothConnection.imageset/ic_bluetooth_connected_3x.png
new file mode 100644
index 0000000..f6d34ae
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/bluetoothConnection.imageset/ic_bluetooth_connected_3x.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/checkmark.imageset/Contents.json b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/checkmark.imageset/Contents.json
new file mode 100644
index 0000000..07764d4
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/checkmark.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "checkmark.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/checkmark.imageset/checkmark.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/checkmark.imageset/checkmark.png
new file mode 100644
index 0000000..c977734
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/checkmark.imageset/checkmark.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/disclosureIndicator.imageset/Contents.json b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/disclosureIndicator.imageset/Contents.json
new file mode 100644
index 0000000..bd38d82
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/disclosureIndicator.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "disclosureIndicator.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/disclosureIndicator.imageset/disclosureIndicator.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/disclosureIndicator.imageset/disclosureIndicator.png
new file mode 100644
index 0000000..c36e050
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/disclosureIndicator.imageset/disclosureIndicator.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/settings.imageset/Contents.json b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/settings.imageset/Contents.json
new file mode 100644
index 0000000..76f97e1
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/settings.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "ic_settings_white.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_settings_white_2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_settings_white_3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/settings.imageset/ic_settings_white.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/settings.imageset/ic_settings_white.png
new file mode 100644
index 0000000..8909c35
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/settings.imageset/ic_settings_white.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/settings.imageset/ic_settings_white_2x.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/settings.imageset/ic_settings_white_2x.png
new file mode 100644
index 0000000..5caedc8
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/settings.imageset/ic_settings_white_2x.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/settings.imageset/ic_settings_white_3x.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/settings.imageset/ic_settings_white_3x.png
new file mode 100644
index 0000000..eabb0a2
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/settings.imageset/ic_settings_white_3x.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/unlock.imageset/Contents.json b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/unlock.imageset/Contents.json
new file mode 100644
index 0000000..503a19a
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/unlock.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "ic_lock.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_lock_2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "ic_lock_3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/unlock.imageset/ic_lock.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/unlock.imageset/ic_lock.png
new file mode 100644
index 0000000..3a4a158
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/unlock.imageset/ic_lock.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/unlock.imageset/ic_lock_2x.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/unlock.imageset/ic_lock_2x.png
new file mode 100644
index 0000000..2a16694
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/unlock.imageset/ic_lock_2x.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/unlock.imageset/ic_lock_3x.png b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/unlock.imageset/ic_lock_3x.png
new file mode 100644
index 0000000..a7caa2d
Binary files /dev/null and b/tools/gatt-config/ios/Beaconfig/Beaconfig/Assets.xcassets/unlock.imageset/ic_lock_3x.png differ
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Base.lproj/LaunchScreen.storyboard b/tools/gatt-config/ios/Beaconfig/Beaconfig/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..997b044
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Base.lproj/Main.storyboard b/tools/gatt-config/ios/Beaconfig/Beaconfig/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..0aa6c3a
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Base.lproj/Main.storyboard
@@ -0,0 +1,392 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconDetailsViewController.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconDetailsViewController.swift
new file mode 100644
index 0000000..a63b405
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconDetailsViewController.swift
@@ -0,0 +1,517 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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 UIKit
+import CoreBluetooth
+
+enum ChangeLockCodeStatus {
+ case Success
+ case IncorrectOldPasskey
+ case Fail
+}
+
+let kButtonWidth: CGFloat = 110
+
+class BeaconDetailsViewController: UIViewController,
+ UIGestureRecognizerDelegate {
+ var beacon: CBPeripheral?
+ var beaconScanner: BeaconScanner?
+ var beaconGATTOperations: GATTOperations?
+ var slotDataContentViews: [SlotDataContentView] = []
+ @IBOutlet weak var slotTabsScrollView: UIScrollView!
+ @IBOutlet weak var beaconNameLabel: UILabel!
+ @IBOutlet weak var tabsContentView: UIView!
+ @IBOutlet weak var beaconConnectionView: UIView!
+ @IBOutlet weak var saveButton: UIBarButtonItem!
+ var unlockView: UIView?
+ var globalView: SlotDataContentView!
+ var previousButton: UIButton?
+ var beaconName: String?
+ var slotViews: [UIView] = []
+ var beaconPasskey: String?
+ var beaconInvestigation: BeaconInvestigation?
+ var beaconConfiguration: ConfigureBeaconSlotData?
+ var slotData: Dictionary > = [:]
+ var slotUpdateData: Dictionary > = [:]
+ var beaconCapabilities: NSDictionary = [:]
+ var connectionViewObject: ConnectionView!
+ var scrollingView: HorizontalScrollButtonList?
+ var connectionTimer: NSTimer?
+ ///
+ /// the passkey must have 16 bytes in hex format, which makes it
+ /// 32 characters when inserted as String
+ ///
+ let passkeyRequiredLength = 32
+ var scanningTime = 0
+ var timer: NSTimer?
+ var viewToDisable: UIView?
+ var selectHolder: UIView?
+ var userInfo: UserInformation!
+ var throbberAlert: UIAlertController!
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ if let name = self.beaconName {
+ beaconNameLabel.text = name
+ }
+ tabsContentView.backgroundColor = kLightGrayColor
+ slotTabsScrollView.backgroundColor = kLightGrayColor
+ beaconConnectionView.backgroundColor = kLightGrayColor
+ /// Dismiss keyboard when user touches outside it.
+ let tap: UITapGestureRecognizer =
+ UITapGestureRecognizer(target: self,
+ action: #selector(UIInputViewController.dismissKeyboard))
+ tap.delegate = self
+ view.addGestureRecognizer(tap)
+ /// Swipe the tab views left and right.
+ let swipeRight = UISwipeGestureRecognizer(target: self,
+ action: #selector(BeaconDetailsViewController.respondToSwipeGesture(_:)))
+ let swipeLeft = UISwipeGestureRecognizer(target: self,
+ action: #selector(BeaconDetailsViewController.respondToSwipeGesture(_:)))
+ swipeRight.direction = UISwipeGestureRecognizerDirection.Right
+ swipeLeft.direction = UISwipeGestureRecognizerDirection.Left
+ self.view.addGestureRecognizer(swipeRight)
+ self.view.addGestureRecognizer(swipeLeft)
+ connectToBeacon()
+ }
+
+ func dismissKeyboard() {
+ view.endEditing(true)
+ }
+
+ func slotButtonPressed(buttonNumber: Int) {
+ for view in slotViews {
+ if !view.hidden {
+ view.hidden = true
+ }
+ }
+ slotViews[buttonNumber].hidden = false
+ }
+
+ func signInWithGoogle(viewToDisable: UIView, selectHolder: UIView) {
+ self.viewToDisable = viewToDisable
+ self.selectHolder = selectHolder
+ self.performSegueWithIdentifier("signInSegue", sender: self)
+ }
+
+ ///
+ /// When the user decides to save the changes for the beacon configuration, we want to
+ /// collect all the information and to create a dictionary with it. We must also display
+ /// errors in case if we know that the desired configuration is incorrect.
+ ///
+ @IBAction func saveBeaconData(sender: AnyObject) {
+ view.endEditing(true)
+ slotUpdateData.removeAll()
+ var EIDConfiguration = false
+ var URLConflict = false
+ var i = 0
+ for slotView in slotDataContentViews {
+ if slotUpdateData[i] == nil {
+ slotUpdateData[i] = [:]
+ }
+ if let data = slotView.getUpdateData() {
+ if data[slotDataEIDKey] != nil {
+ EIDConfiguration = true
+ }
+ slotUpdateData[i] = data
+ } else {
+ return
+ }
+ i += 1
+ }
+
+ if EIDConfiguration {
+ i = 0
+ for _ in slotDataContentViews {
+ if slotUpdateData[i]?[slotDataURLKey] != nil {
+ URLConflict = true
+ }
+ i += 1
+ }
+ }
+
+ ///
+ /// Broadcasting an URL frame while also broadcasting EID is a huge security problem
+ /// and it should not be allowed, because it makes the beacon easily recognisable.
+ ///
+ if URLConflict {
+ showAlert("URL Conflict",
+ description: "Broadcasting URL while also broadcasting EID would " +
+ "make your beacon easy to be recognised. Please " +
+ "consider removing the URL frame.",
+ buttonText: "Dismiss")
+ } else {
+ for slotView in slotDataContentViews {
+ slotView.slotContentView.removeFromSuperview()
+ }
+ displayThrobber("Saving...")
+ if let currentBeacon = beacon {
+ /// We have all the data we need in order to update the beacon's configuration.
+ beaconConfiguration = ConfigureBeaconSlotData(peripheral: currentBeacon)
+ beaconConfiguration!.beginBeaconConfiguration(beaconCapabilities,
+ statusInfoAlert: throbberAlert!,
+ slotUpdateData: slotUpdateData) {
+ dispatch_async(dispatch_get_main_queue()) {
+ ///
+ /// If everything in the update went well,
+ /// we want to investigate the beacon
+ /// again, because some values, such as
+ /// advertising interval, can be changed
+ /// by the beacon with the closest
+ /// available value.
+ ///
+ self.investigateBeacon()
+ }
+ }
+ }
+ }
+ }
+
+ /// Creates the Global settings tab view for specific beacon configurations.
+ func createGlobalView() -> UIView {
+ globalView = SlotDataContentView()
+ slotDataContentViews.append(globalView)
+ if let data = slotData[0] {
+ globalView.setAsGlobalContentView(beaconCapabilities, slotData: data)
+ }
+ slotDataContentViews.last!.setUICallbacks(changeLockCode,
+ factoryResetCallback: startFactoryReset,
+ remainConnectableCallback: changeRemainConnectable,
+ showAlert: showAlert,
+ signIn: signInWithGoogle)
+ return slotDataContentViews.last!.slotContentView
+ }
+
+ func changeRemainConnectable(on: Bool) {
+ displayThrobber("Changing state...")
+ if let operations = beaconGATTOperations {
+ operations.changeRemainConnectableState(on) {
+ dispatch_async(dispatch_get_main_queue()) {
+ self.dismissViewControllerAnimated(false, completion: nil)
+ }
+ }
+ }
+ }
+
+ func startFactoryReset() {
+ displayThrobber("Factory resetting...")
+ if let operations = beaconGATTOperations {
+ operations.factoryReset() {
+ dispatch_async(dispatch_get_main_queue()) {
+ for view in self.slotDataContentViews {
+ view.slotContentView.removeFromSuperview()
+ }
+ self.slotDataContentViews.removeAll()
+ self.investigateBeacon()
+ }
+ }
+ }
+ }
+
+ func changeLockCode(oldCode: String, newCode: String) {
+ if oldCode == beaconPasskey {
+ displayThrobber("Changing lock code...")
+ if let operations = beaconGATTOperations {
+ operations.changeLockCode(oldCode, newCode: newCode) { lockState in
+ dispatch_async(dispatch_get_main_queue()) {
+ self.dismissViewControllerAnimated(false, completion: nil)
+ if lockState == LockState.Unlocked {
+ self.globalView.lockCodeChanged()
+ } else {
+ ///
+ /// The lock code is changed and it is not the one that the user wanted.
+ /// This IS a disaster.
+ ///
+ assert(false)
+ }
+ }
+ }
+ }
+ } else {
+ self.showAlert("Passkey",
+ description: "The old passkey is incorrect.",
+ buttonText: "Dismiss")
+ }
+ }
+
+ func respondToSwipeGesture(gesture: UIGestureRecognizer) {
+
+ if let swipeGesture = gesture as? UISwipeGestureRecognizer {
+
+ switch swipeGesture.direction {
+
+ case UISwipeGestureRecognizerDirection.Right:
+ scrollingView?.swipeRight()
+ case UISwipeGestureRecognizerDirection.Left:
+ scrollingView?.swipeLeft()
+ default:
+ break
+ }
+ }
+ }
+
+ func slotDataContentView(slotNumber: Int, slotData: Dictionary ) -> UIView? {
+ /// Creates the content views for each slot of the beacon
+ let object = SlotDataContentView()
+ slotDataContentViews.append(object)
+ if let frameType = slotData[slotDataFrameTypeKey] {
+ let frameTypeName = NSString(data:frameType, encoding:NSUTF8StringEncoding) as String!
+ switch frameTypeName {
+ case BeaconInfo.EddystoneFrameType.URLFrameType.description:
+ slotDataContentViews.last!.setAsURLTypeContentView(slotData,
+ capabilities: beaconCapabilities)
+ case BeaconInfo.EddystoneFrameType.UIDFrameType.description:
+ slotDataContentViews.last!.setAsUIDTypeContentView(slotData,
+ capabilities: beaconCapabilities)
+ case BeaconInfo.EddystoneFrameType.TelemetryFrameType.description:
+ slotDataContentViews.last!.setAsTLMTypeContentView(slotData,
+ capabilities: beaconCapabilities)
+ case BeaconInfo.EddystoneFrameType.EIDFrameType.description:
+ slotDataContentViews.last!.setAsEIDTypeContentView(slotData,
+ capabilities: beaconCapabilities)
+ default:
+ slotDataContentViews.last!.setAsNoFrameTypeContentView(beaconCapabilities)
+ }
+ }
+ slotDataContentViews.last!.setUICallbacks(changeLockCode,
+ factoryResetCallback: startFactoryReset,
+ remainConnectableCallback: changeRemainConnectable,
+ showAlert: showAlert,
+ signIn: signInWithGoogle)
+ return slotDataContentViews.last!.slotContentScrollView
+ }
+
+ func showAlert(title: String, description: String, buttonText:String) {
+ let alertController = UIAlertController(title: title,
+ message: description,
+ preferredStyle: UIAlertControllerStyle.Alert)
+ alertController.addAction(UIAlertAction(title: buttonText,
+ style: UIAlertActionStyle.Default,
+ handler: nil))
+ alertController.view.tintColor = kGreenColor
+
+ self.presentViewController(alertController, animated: true, completion: nil)
+ }
+
+ func displayThrobber(message: String) {
+ throbberAlert = UIAlertController(title: nil, message: message, preferredStyle: .Alert)
+
+ throbberAlert.view.tintColor = UIColor.blackColor()
+ /// Need to give UIActivityIndicator nonzero values at init.
+ let loadingIndicator: UIActivityIndicatorView =
+ UIActivityIndicatorView(frame: CGRectMake(10, 5, 50, 50)) as UIActivityIndicatorView
+ loadingIndicator.hidesWhenStopped = true
+ loadingIndicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.Gray
+ loadingIndicator.startAnimating()
+ throbberAlert.view.addSubview(loadingIndicator)
+ self.presentViewController(throbberAlert, animated: true, completion: nil)
+ }
+
+ func unlockBeacon(passkey: String) {
+ dismissKeyboard()
+ if let
+ beaconOperations = beaconGATTOperations {
+ beaconOperations.beginUnlockingBeacon(passkey) { lockState in
+ dispatch_async(dispatch_get_main_queue()) {
+ if lockState == LockState.Locked {
+ /// User inserted a wrong password.
+ self.showAlert("Password",
+ description: "The password is incorrect.",
+ buttonText: "Dismiss")
+ } else if lockState == LockState.Unlocked {
+ /// The beacon is now unlocked!
+ self.beaconPasskey = passkey
+ self.displayThrobber("Reading slot data...")
+ self.investigateBeacon()
+ }
+ }
+ }
+ }
+ }
+
+ func displayScrollBar(beaconSlotDataCount: Int) {
+ // +1 for 'Global' settings button / tab
+ let buttonsToDisplay = beaconSlotDataCount + 1
+ let buttonNames: NSMutableArray = []
+ for i in 0...buttonsToDisplay - 1 {
+ if i == 0 {
+ buttonNames.addObject("GLOBAL")
+ } else {
+ buttonNames.addObject("SLOT \(i)")
+ }
+ }
+ if scrollingView == nil {
+ scrollingView = HorizontalScrollButtonList(buttonSize:
+ CGSizeMake(kButtonWidth,
+ slotTabsScrollView.frame.height - 5),
+ buttonNames: buttonNames,
+ callback: slotButtonPressed(_:))
+ self.slotTabsScrollView.addSubview(scrollingView!)
+ self.slotTabsScrollView.showsHorizontalScrollIndicator = true
+ self.slotTabsScrollView.indicatorStyle = .Default
+ self.slotTabsScrollView.contentSize = CGSize(width: scrollingView!.frame.size.width,
+ height: 1.0)
+ self.slotTabsScrollView.backgroundColor = UIColor.whiteColor()
+ } else {
+ scrollingView!.slotButtonPressed(scrollingView!.previousButtonPressed!)
+ }
+ }
+
+ func createContentViews(beaconDataSlotsCount: Int) {
+ slotViews.removeAll()
+ slotDataContentViews.removeAll()
+ let globalView = createGlobalView()
+ slotViews.append(globalView)
+ tabsContentView.addSubview(globalView)
+ if slotDataContentViews.count == 0 {
+ globalView.hidden = false
+ }
+ for i in 0...beaconDataSlotsCount - 1 {
+ if let data = slotData[i as NSNumber] {
+ if let slotView = slotDataContentView(i, slotData: data) {
+ slotView.hidden = true
+ slotViews.append(slotView)
+ self.tabsContentView.addSubview(slotView)
+ }
+ }
+ }
+ }
+
+ func investigateBeacon() {
+ if let connectedBeacon = self.beacon {
+ self.beaconInvestigation = BeaconInvestigation(peripheral: connectedBeacon)
+ if let investigation = self.beaconInvestigation {
+ investigation.finishedUnlockingBeacon() { beaconCapabilities, slotData in
+ /// Creates the buttons and the pages for them and populate them with the information.
+ dispatch_async(dispatch_get_main_queue()) {
+ self.beaconConnectionView.hidden = true
+ self.dismissViewControllerAnimated(false, completion: nil)
+ self.slotData = slotData
+ self.beaconCapabilities = beaconCapabilities
+ if let beaconDataSlotsCount = beaconCapabilities[maxSupportedSlotsKey] as? NSNumber {
+ self.createContentViews(beaconDataSlotsCount as Int)
+ self.displayScrollBar(beaconDataSlotsCount as Int)
+ self.saveButton.enabled = true
+ }
+ }
+ }
+ }
+ }
+ }
+
+ func prepareForUseWithBeacon(beaconScanner: BeaconScanner,
+ beacon: CBPeripheral,
+ operations: GATTOperations,
+ beaconName: String?,
+ userInfo: UserInformation) {
+ self.beaconScanner = beaconScanner
+ self.beacon = beacon
+ self.beaconGATTOperations = operations
+ self.beaconName = beaconName
+ self.userInfo = userInfo
+ }
+
+ func activityIndicatorChanger() {
+ self.connectionViewObject.stateTextView!.text.appendContentsOf(".")
+ }
+
+ func connectToBeacon() {
+ connectionViewObject = ConnectionView()
+ connectionTimer = NSTimer.scheduledTimerWithTimeInterval(
+ 1.0,
+ target: self,
+ selector: #selector(BeaconDetailsViewController.activityIndicatorChanger),
+ userInfo: nil,
+ repeats: true)
+
+ let connectionView = connectionViewObject.configureView(connectToBeacon,
+ unlockBeaconCallback: unlockBeacon)
+ beaconConnectionView.addSubview(connectionView)
+ if let
+ scanner = beaconScanner,
+ currentBeacon = beacon {
+ scanner.connectToBeacon(currentBeacon) { operationState in
+ dispatch_async(dispatch_get_main_queue()) {
+ ///
+ /// There are several reasons why a connection to the beacon cannot be established,
+ /// some of them being that the beacon does not implement the GATT Configuration
+ /// Service, that the beacon is too far or that it is not in a connectable state.
+ ///
+ if let
+ _ = operationState.name,
+ errorDescription = operationState.description {
+ self.connectionTimer?.invalidate()
+ self.connectionViewObject.stateTextView!.text = errorDescription
+ self.connectionViewObject.addRetryButton()
+ } else {
+ ///
+ /// If we managed to connect to the beacon, we want to check its lock state and
+ /// as the user to unlock it if that's necessary.
+ ///
+ self.connectionTimer?.invalidate()
+ self.connectionViewObject.stateTextView!.text = "Connection successful!"
+ self.checkLockState()
+ }
+ }
+ }
+ }
+ }
+
+ func checkLockState() {
+ saveButton.enabled = false
+ if let
+ beaconOperations = beaconGATTOperations {
+ beaconOperations.checkLockState(nil) { lockState in
+ dispatch_async(dispatch_get_main_queue()) {
+ if lockState == LockState.Locked {
+ /// Display the view that asks the user to insert passkey for unlocking.
+ self.connectionViewObject.createUnlockingView()
+ } else if lockState == LockState.Unlocked
+ || lockState == LockState.UnlockedPreventAutolock {
+ /// We have an unlocked beacon. We can now start reading all its slot data.
+ self.displayThrobber("Reading slot data...")
+ self.investigateBeacon()
+ }
+ }
+ }
+ }
+ }
+
+ override func viewWillDisappear(animated : Bool) {
+ super.viewWillDisappear(animated)
+
+ ///
+ /// The user pressed the "Back" key. There's no reason for us to mentain the connection
+ /// to the beacon and we're disconnecting.
+ ///
+ if (self.isMovingFromParentViewController()){
+ if let peripheral = beacon, scanner = beaconScanner {
+ scanner.disconnectFromBeacon(peripheral)
+ }
+ }
+ }
+
+ override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
+ if segue.identifier == "signInSegue" {
+ if let
+ signInController = segue.destinationViewController as? UserLoginViewController,
+ view = viewToDisable,
+ selectView = selectHolder {
+ signInController.userInfo = userInfo
+ signInController.viewToDisable = view
+ signInController.selectHolder = selectView
+ }
+ }
+ }
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconFramesSupported.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconFramesSupported.swift
new file mode 100644
index 0000000..2499438
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconFramesSupported.swift
@@ -0,0 +1,67 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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 UIKit
+
+///
+/// Identifies the frames broadcasted by a beacon during the scan and
+/// parses the information for each type. It is used to provide the frame
+/// types for each beacon.
+///
+class BeaconFramesSupported {
+ var UIDFrameSupported: Bool = false
+ var EIDFrameSupported: Bool = false
+ var TLMFrameSupported: Bool = false
+ var URLFrameSupported: Bool = false
+ var EddystoneGATTServiceFrameSupported: Bool = false
+ var URLData: String?
+ var UIDData: String?
+ var EIDData: String?
+ var TLMData: String?
+ var operations: GATTOperations
+
+ init(operations: GATTOperations) {
+ self.operations = operations
+ }
+
+ /// We check if the beacon we scanned for has any frame that is useful for us.
+ func containsUsefulFrames() -> Bool {
+ return UIDFrameSupported || EIDFrameSupported || EddystoneGATTServiceFrameSupported ||
+ URLFrameSupported || TLMFrameSupported
+ }
+
+ func setUIDframe(UID: String) {
+ UIDFrameSupported = true
+ UIDData = UID
+ }
+
+ func setEIDframe(EID: String) {
+ EIDFrameSupported = true
+ EIDData = EID
+ }
+
+ func setTLMframe(TLM: String) {
+ TLMFrameSupported = true
+ TLMData = TLM
+ }
+
+ func setURLframe(URL: String) {
+ URLFrameSupported = true
+ URLData = URL
+ }
+
+ func setEddystoneGATTServiceFrame() {
+ EddystoneGATTServiceFrameSupported = true
+ }
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconInvestigation.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconInvestigation.swift
new file mode 100644
index 0000000..d77347c
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconInvestigation.swift
@@ -0,0 +1,391 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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 UIKit
+import Foundation
+import CoreBluetooth
+
+let kBroadCastCapabilitiesVersion: UInt8 = 0
+let kBroadcastCapabilitiesPerSlotAdvIntervals: UInt8 = 0x1
+let kBroadcastCapabilitiesPerSlotTxPower: UInt8 = 0x2
+let kBroadcastCapabilitiesSupportsUID: UInt8 = 0x1
+let kBroadcastCapabilitiesSupportsURL: UInt8 = 0x2
+let kBroadcastCapabilitiesSupportsTLM: UInt8 = 0x4
+let kBroadcastCapabilitiesSupportsEID: UInt8 = 0x8
+
+let maxSupportedSlotsKey = "maxSupportedSlots"
+let maxSupportedEIDSlotsKey = "maxSupportedEIDSlots"
+let perSlotAdvIntervalsSupportedKey = "advertismentIntervalSupported"
+let perSlotTxPowerSupportedKey = "txPowerSupported"
+let UIDSupportedKey = "UIDSupportedKey"
+let URLSupportedKey = "URLSupportedKey"
+let TLMSupportedKey = "TLMSupportedKey"
+let EIDSupportedKey = "EIDSupportedKey"
+let BroadcastCapabilitiesSupportsEID = "BroadcastCapabilitiesSupportsEID"
+
+let slotDataFrameTypeKey = "slotDataFrameTypeKey"
+let slotDataUIDKey = "slotDataUIDKey"
+let slotDataURLKey = "slotDataURLKey"
+let slotDataTLMKey = "slotDataTLMKey"
+let slotDataEIDKey = "slotDataEIDKey"
+let slotDataAdvIntervalKey = "slotDataAdvIntervalKey"
+let slotDataTxPowerKey = "slotDataTxPowerKey"
+let slotDataNoFrameKey = "slotDataNoFrameKey"
+let slotDataRemainConnectableKey = "slotDataRemainCOnnectableKey"
+
+
+///
+/// The class wants to read all the information for the beacon.
+/// We have to discover the capabilities and get information such as the maximum number of
+/// slots that are available, and then, for each slot, we want to read the TX Power, the
+/// advertising interval and the actual slot data. We actually created a nice flow for this
+/// to make things look cleaner and to be able to handle errors.
+///
+class BeaconInvestigation: GATTOperations {
+
+ var currentlyScannedSlot: UInt8 = 0
+ var beaconBroadcastCapabilities: NSDictionary = [:]
+ var slotData = [NSNumber : [ String : NSData]]()
+ var callback: ((beaconBroadcastCapabilities: NSDictionary,
+ slotData: Dictionary >) -> Void)?
+
+ enum InvestigationState {
+ case BeaconUnlocked
+ case DiscoveredCapabilities
+ case ErrorDiscoveringCapabilities
+ case DidSetActiveSlot
+ case ErrorSettingActiveSlot
+ case ScannedSlot
+ case ScannedAllSlots
+ case ErrorScanningSlot
+ case DidReadTxPower
+ case ErrorReadingTxPower
+ case DidReadAdvertisingInterval
+ case ErrorReadingAdvertisingInterval
+ case DidReadRemainConnectableState
+ case ErrorReadingRemainCOnectableState
+ }
+
+ func didUpdateInvestigationState(investigationState: InvestigationState) {
+ switch investigationState {
+ case InvestigationState.BeaconUnlocked:
+ discoverCapabilities()
+ case InvestigationState.DiscoveredCapabilities:
+ setSlotForScan()
+ case InvestigationState.DidSetActiveSlot:
+ scanSlot()
+ case InvestigationState.ScannedSlot:
+ readTxPower()
+ case InvestigationState.DidReadTxPower:
+ readAdvertisingInterval()
+ case InvestigationState.DidReadAdvertisingInterval:
+ readRemainConnectableState()
+ case InvestigationState.DidReadRemainConnectableState:
+ setSlotForScan()
+ case InvestigationState.ScannedAllSlots:
+ if let investigationCallback = callback {
+ investigationCallback(beaconBroadcastCapabilities: beaconBroadcastCapabilities,
+ slotData: slotData)
+ }
+ case InvestigationState.ErrorScanningSlot:
+ currentlyScannedSlot += 1;
+ setSlotForScan()
+ default:
+ return
+ }
+ }
+
+ func finishedUnlockingBeacon(investigationCallback:
+ (beaconBroadcastCapabilities: NSDictionary,
+ slotData: Dictionary >) -> Void) {
+ callback = investigationCallback
+ peripheral.delegate = self
+ didUpdateInvestigationState(InvestigationState.BeaconUnlocked)
+ }
+
+ func discoverCapabilities() {
+ if let characteristic = findCharacteristicByID(CharacteristicID.capabilities.UUID) {
+ peripheral.readValueForCharacteristic(characteristic)
+ }
+ }
+
+ override func peripheral(peripheral: CBPeripheral,
+ didUpdateValueForCharacteristic characteristic: CBCharacteristic,
+ error: NSError?) {
+ NSLog("did update value for characteristic \(characteristic.UUID)")
+ if let identifiedError = error {
+ NSLog("Error reading characteristic: \(identifiedError)")
+ } else {
+ switch characteristic.UUID {
+ case CharacteristicID.capabilities.UUID:
+ didReadBroadcastCapabilities()
+ case CharacteristicID.ADVSlotData.UUID:
+ didReadSlotData()
+ case CharacteristicID.radioTxPower.UUID:
+ didReadTxPower()
+ case CharacteristicID.advertisingInterval.UUID:
+ didReadAdvertisingInterval()
+ case CharacteristicID.remainConnectable.UUID:
+ didReadRemainConnectableState()
+ default:
+ return
+ }
+ }
+ }
+
+ override func peripheral(peripheral: CBPeripheral,
+ didWriteValueForCharacteristic characteristic: CBCharacteristic,
+ error: NSError?) {
+
+ NSLog("did write value for characteristic \(characteristic.UUID)")
+ if characteristic.UUID == CharacteristicID.activeSlot.UUID {
+ if error != nil {
+ didUpdateInvestigationState(InvestigationState.ErrorSettingActiveSlot)
+ } else {
+ didUpdateInvestigationState(InvestigationState.DidSetActiveSlot)
+ }
+ }
+ }
+
+ struct EddystoneGATTBroadcastCapabilities {
+ var version: UInt8
+ var maxSupportedTotalSlots: UInt8
+ var maxSupportedEidSlots: UInt8
+ var capabilitiesBitField: UInt8
+ var supportedFrameTypesBitFieldHigh: UInt8
+ var supportedFrameTypesBitFieldLow: UInt8
+
+ init() {
+ version = 0
+ maxSupportedTotalSlots = 0
+ maxSupportedEidSlots = 0
+ supportedFrameTypesBitFieldLow = 0
+ supportedFrameTypesBitFieldHigh = 0
+ capabilitiesBitField = 0
+ }
+ }
+
+ func isBitOn(field: UInt8, mask: UInt8) -> Bool {
+ return (field & mask) != 0
+ }
+
+ func didReadBroadcastCapabilities() {
+ if let
+ capabilitiesCharacteristic = findCharacteristicByID(CharacteristicID.capabilities.UUID),
+ capabilities = capabilitiesCharacteristic.value {
+
+ if capabilities.length < sizeof(EddystoneGATTBroadcastCapabilities) {
+ didUpdateInvestigationState(InvestigationState.ErrorDiscoveringCapabilities)
+ } else {
+ var broadcastCapabilities = EddystoneGATTBroadcastCapabilities()
+ capabilities.getBytes(&broadcastCapabilities, length: capabilities.length)
+ if broadcastCapabilities.version != kBroadCastCapabilitiesVersion {
+ didUpdateInvestigationState(InvestigationState.ErrorDiscoveringCapabilities)
+ } else {
+ let txPowers: NSMutableArray = []
+ var i = 6
+ while i < capabilities.length {
+ var txPower: Int8 = 0
+ capabilities.getBytes(&txPower, range:NSMakeRange(i, sizeof(Int8)))
+ txPowers.addObject(NSNumber(char: txPower))
+ i += 1
+ }
+
+ beaconBroadcastCapabilities =
+ [maxSupportedSlotsKey :
+ NSNumber(unsignedChar: broadcastCapabilities.maxSupportedTotalSlots),
+ maxSupportedEIDSlotsKey :
+ NSNumber(unsignedChar: broadcastCapabilities.maxSupportedEidSlots),
+ perSlotAdvIntervalsSupportedKey :
+ isBitOn(broadcastCapabilities.capabilitiesBitField,
+ mask: kBroadcastCapabilitiesPerSlotAdvIntervals),
+ perSlotTxPowerSupportedKey :
+ isBitOn(broadcastCapabilities.capabilitiesBitField,
+ mask: kBroadcastCapabilitiesPerSlotTxPower),
+ UIDSupportedKey :
+ isBitOn(broadcastCapabilities.supportedFrameTypesBitFieldLow,
+ mask: kBroadcastCapabilitiesSupportsUID),
+ URLSupportedKey :
+ isBitOn(broadcastCapabilities.supportedFrameTypesBitFieldLow,
+ mask: kBroadcastCapabilitiesSupportsURL),
+ TLMSupportedKey :
+ isBitOn(broadcastCapabilities.supportedFrameTypesBitFieldLow,
+ mask: kBroadcastCapabilitiesSupportsTLM),
+ EIDSupportedKey :
+ isBitOn(broadcastCapabilities.supportedFrameTypesBitFieldLow,
+ mask: kBroadcastCapabilitiesSupportsEID)]
+ currentlyScannedSlot = 0
+ didUpdateInvestigationState(InvestigationState.DiscoveredCapabilities)
+ }
+ }
+ }
+ }
+
+ func setSlotForScan() {
+ if let maxSupportedSlots = beaconBroadcastCapabilities[maxSupportedSlotsKey] {
+ let intMaxSupportedSlots: Int = maxSupportedSlots as! Int
+ if currentlyScannedSlot >= UInt8(intMaxSupportedSlots) {
+ didUpdateInvestigationState(InvestigationState.ScannedAllSlots)
+ } else {
+ if let characteristic = findCharacteristicByID(CharacteristicID.activeSlot.UUID) {
+ let currentSlot = NSData(bytes: ¤tlyScannedSlot, length: 1)
+ peripheral.writeValue(currentSlot,
+ forCharacteristic: characteristic,
+ type: CBCharacteristicWriteType.WithResponse)
+ }
+ }
+ }
+ }
+
+ func scanSlot() {
+ if let characteristic = findCharacteristicByID(CharacteristicID.ADVSlotData.UUID) {
+ peripheral.readValueForCharacteristic(characteristic)
+ }
+ }
+
+ func didReadSlotData() {
+ if let
+ characteristic = findCharacteristicByID(CharacteristicID.ADVSlotData.UUID),
+ value = characteristic.value {
+ updateSlotData(value)
+ }
+ }
+
+ func updateSlotData(value: NSData) {
+ var frameTypeName: String!
+ if value.length > 0 {
+ let scannedSlot: NSNumber = NSNumber(unsignedChar: currentlyScannedSlot)
+ var frameType: UInt8 = 0
+ value.getBytes(&frameType, range: NSMakeRange(0, 1))
+
+ if slotData[scannedSlot] == nil {
+ slotData[scannedSlot] = [String : NSData]()
+ }
+
+ if frameType == BeaconInfo.EddystoneUIDFrameTypeID {
+ frameTypeName = BeaconInfo.EddystoneFrameType.UIDFrameType.description
+ slotData[scannedSlot]![slotDataFrameTypeKey] =
+ frameTypeName.dataUsingEncoding(NSUTF8StringEncoding)
+ ///
+ /// If the frame doesn't have enough characters, it means that the UID is malformed
+ /// and no data is saved for this slot.
+ ///
+ if value.length >= 18 {
+ ///
+ /// The first two bytes represent frame type and ranging data. The rest of 16 bytes
+ /// represent the UID - 10-byte Namespace and 6-byte Instance. If the frame has more
+ /// bytes, we will simply truncate it to the ones we need.
+ ///
+ slotData[scannedSlot]![slotDataUIDKey] = value.subdataWithRange(NSMakeRange(2, 16))
+ }
+ } else if frameType == BeaconInfo.EddystoneURLFrameTypeID {
+ frameTypeName = BeaconInfo.EddystoneFrameType.URLFrameType.description
+ slotData[scannedSlot]![slotDataFrameTypeKey] =
+ frameTypeName.dataUsingEncoding(NSUTF8StringEncoding)
+ if let
+ urlData = BeaconInfo.parseURLFromFrame(value),
+ urlNSData = urlData.absoluteString!.dataUsingEncoding(NSUTF8StringEncoding) {
+ NSLog("\(slotData[scannedSlot]![slotDataFrameTypeKey])")
+ slotData[scannedSlot]![slotDataURLKey] = urlNSData
+ }
+ } else if frameType == BeaconInfo.EddystoneTLMFrameTypeID {
+ frameTypeName = BeaconInfo.EddystoneFrameType.TelemetryFrameType.description
+ slotData[scannedSlot]![slotDataFrameTypeKey] =
+ frameTypeName.dataUsingEncoding(NSUTF8StringEncoding)
+ slotData[scannedSlot]![slotDataTLMKey] = value
+ } else if frameType == BeaconInfo.EddystoneEIDFrameTypeID {
+ frameTypeName = BeaconInfo.EddystoneFrameType.EIDFrameType.description
+ slotData[scannedSlot]![slotDataFrameTypeKey] =
+ frameTypeName.dataUsingEncoding(NSUTF8StringEncoding)
+ }
+ didUpdateInvestigationState(InvestigationState.ScannedSlot)
+ } else {
+ let scannedSlot: NSNumber = NSNumber(unsignedChar: currentlyScannedSlot)
+ if slotData[scannedSlot] == nil {
+ slotData[scannedSlot] = [String : NSData]()
+ }
+ frameTypeName = BeaconInfo.EddystoneFrameType.NotSetFrameType.description
+ slotData[scannedSlot]![slotDataFrameTypeKey] =
+ frameTypeName.dataUsingEncoding(NSUTF8StringEncoding)
+
+ didUpdateInvestigationState(InvestigationState.ErrorScanningSlot)
+ }
+ }
+
+ func readTxPower() {
+ if let characteristic = findCharacteristicByID(CharacteristicID.radioTxPower.UUID) {
+ peripheral.readValueForCharacteristic(characteristic)
+ }
+ }
+
+ func getValueForCharacteristic(characteristicID: CBUUID) -> NSData? {
+ if let
+ characteristic = findCharacteristicByID(characteristicID),
+ value = characteristic.value {
+ return value
+ }
+ return nil
+ }
+
+ func didReadTxPower() {
+ let scannedSlot: NSNumber = NSNumber(unsignedChar: currentlyScannedSlot)
+ var txPower: Int8 = 0
+ if let value = getValueForCharacteristic(CharacteristicID.radioTxPower.UUID) {
+ value.getBytes(&txPower, length: sizeof(Int8))
+ }
+ if slotData[scannedSlot] != nil {
+ slotData[scannedSlot]![slotDataTxPowerKey] = NSData(bytes: &txPower, length: sizeof(Int8))
+ }
+ didUpdateInvestigationState(InvestigationState.DidReadTxPower)
+ }
+
+ func readAdvertisingInterval() {
+ if let characteristic = findCharacteristicByID(CharacteristicID.advertisingInterval.UUID) {
+ peripheral.readValueForCharacteristic(characteristic)
+ }
+ }
+
+
+ func didReadAdvertisingInterval() {
+ let scannedSlot: NSNumber = NSNumber(unsignedChar: currentlyScannedSlot)
+ var advertisingInterval: UInt16 = 0
+ if let value = getValueForCharacteristic(CharacteristicID.advertisingInterval.UUID) {
+ value.getBytes(&advertisingInterval, length: sizeof(UInt16))
+ }
+ if slotData[scannedSlot] != nil {
+ var littleEndianAdvInterval: UInt16 = CFSwapInt16BigToHost(advertisingInterval)
+ let bytes = NSData(bytes: &littleEndianAdvInterval,
+ length: sizeof(UInt16))
+ slotData[scannedSlot]![slotDataAdvIntervalKey] = bytes
+ }
+ didUpdateInvestigationState(InvestigationState.DidReadAdvertisingInterval)
+ }
+
+ func readRemainConnectableState() {
+ if let characteristic = findCharacteristicByID(CharacteristicID.remainConnectable.UUID) {
+ peripheral.readValueForCharacteristic(characteristic)
+ }
+ }
+
+ func didReadRemainConnectableState() {
+ let scannedSlot: NSNumber = NSNumber(unsignedChar: currentlyScannedSlot)
+ if let value = getValueForCharacteristic(CharacteristicID.remainConnectable.UUID) {
+ if slotData[scannedSlot] != nil {
+ slotData[scannedSlot]![slotDataRemainConnectableKey] = value
+ }
+ }
+ currentlyScannedSlot += 1
+ didUpdateInvestigationState(InvestigationState.DidReadRemainConnectableState)
+ }
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconItem.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconItem.swift
new file mode 100644
index 0000000..e02a013
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconItem.swift
@@ -0,0 +1,116 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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 UIKit
+import CoreBluetooth
+
+///
+/// We're interested in having all the information that we need about each beacon that we find
+/// while scanning in one place in order to use it when displaying the results. The frames for
+/// each beacon are discovered at different moments of time and we need to be able to update the
+/// information all the time.
+///
+class BeaconItem {
+ var peripheral: CBPeripheral
+ var frames: BeaconFramesSupported
+ var operations: GATTOperations?
+ var framesView: UIView
+
+ init(peripheral: CBPeripheral, frames: BeaconFramesSupported, operations: GATTOperations) {
+ self.peripheral = peripheral
+ self.frames = frames
+ self.operations = operations
+ self.framesView = UIView()
+ var topView: UIView = self.framesView
+ var pinTop: NSLayoutAttribute = .Top
+ if let UID = frames.UIDData {
+ topView = CustomViews.createFrameDisplayView(.UIDFrameType,
+ data:UID,
+ holder: self.framesView,
+ topView: topView,
+ pinTop: pinTop,
+ setBottomConstraints:
+ UIDNeedBottomConstraints(frames))
+ }
+
+ if let EID = frames.EIDData {
+ if topView != self.framesView {
+ pinTop = .Bottom
+ }
+ topView = CustomViews.createFrameDisplayView(.EIDFrameType,
+ data: EID,
+ holder: self.framesView,
+ topView: topView,
+ pinTop: pinTop,
+ setBottomConstraints:
+ EIDNeedBottomConstraints(frames))
+ }
+
+ if let URL = frames.URLData {
+ if topView != self.framesView {
+ pinTop = .Bottom
+ }
+ topView = CustomViews.createFrameDisplayView(.URLFrameType,
+ data:URL,
+ holder: self.framesView,
+ topView: topView,
+ pinTop: pinTop,
+ setBottomConstraints:
+ URLNeedBottomConstraints(frames))
+ }
+
+ if let TLM = frames.TLMData {
+ if topView != self.framesView {
+ pinTop = .Bottom
+ }
+ topView = CustomViews.createFrameDisplayView(.TelemetryFrameType,
+ data: TLM,
+ holder: self.framesView,
+ topView: topView,
+ pinTop: pinTop,
+ setBottomConstraints:
+ TLMNeedBottomConstraints(frames))
+ }
+
+ if frames.EddystoneGATTServiceFrameSupported {
+ if topView != self.framesView {
+ pinTop = .Bottom
+ }
+ CustomViews.displayConfigurableBeacon(self.framesView, topView: topView, pinTop: pinTop)
+ }
+ }
+
+ func UIDNeedBottomConstraints(frames: BeaconFramesSupported) -> Bool {
+ return !frames.EIDFrameSupported &&
+ !frames.URLFrameSupported &&
+ !frames.TLMFrameSupported &&
+ !frames.EddystoneGATTServiceFrameSupported
+ }
+
+ func EIDNeedBottomConstraints(frames: BeaconFramesSupported) -> Bool {
+ return !frames.URLFrameSupported &&
+ !frames.TLMFrameSupported &&
+ !frames.EddystoneGATTServiceFrameSupported
+ }
+
+ func URLNeedBottomConstraints(frames: BeaconFramesSupported) -> Bool {
+ return !frames.TLMFrameSupported &&
+ !frames.EddystoneGATTServiceFrameSupported
+ }
+
+ func TLMNeedBottomConstraints(frames: BeaconFramesSupported) -> Bool {
+ return !frames.EddystoneGATTServiceFrameSupported
+ }
+
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconScanner.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconScanner.swift
new file mode 100644
index 0000000..b002fd9
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconScanner.swift
@@ -0,0 +1,463 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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 UIKit
+import CoreBluetooth
+
+let kBeaconConnectionMaxTimePermitted = 10
+let kGetBeaconsServer = "https://proximitybeacon.googleapis.com/v1beta1/beacons/"
+let kEddystoneEIDCode = 4
+let kBeaconScanTime = 5
+let kBeaconDataRequestTimeout = 15
+
+enum OperationState: String {
+ case Connected
+ case DidDiscoverServices
+ case DiscoveringServicesError
+ case NotImplementingGATTConfigService
+ case DidDiscoverCharacteristics
+ case DiscoveringCharacteristicsError
+ case ConnectionTimeout
+ case CentralManagerConnectionError
+ case DidFailToConnect
+ case Unknown
+
+ var name: String? {
+ switch self {
+ case .DiscoveringServicesError:
+ return "Discovering Services Error"
+ case .DiscoveringCharacteristicsError:
+ return "Discovering Characteristics Error"
+ case .ConnectionTimeout:
+ return "Connection Timeout"
+ case .CentralManagerConnectionError:
+ return "Central Manager Error"
+ case .DidFailToConnect:
+ return "Connect Failed"
+ case .NotImplementingGATTConfigService:
+ return "GATT Configuratin Service"
+ default:
+ return nil
+ }
+ }
+
+ var description: String? {
+ switch self {
+ case .DiscoveringServicesError:
+ return "The services could not be discovered."
+ case .DiscoveringCharacteristicsError:
+ return "The characteristics could not be discovered."
+ case .ConnectionTimeout:
+ return "The process of connecting to the beacon timed out."
+ case .CentralManagerConnectionError:
+ return "Central Manager is not ready."
+ case .DidFailToConnect:
+ return "Connecting to the beacon failed."
+ case .NotImplementingGATTConfigService:
+ return "The beacon does not implement the GATT configuration service."
+ default:
+ return nil
+ }
+ }
+
+}
+
+extension CBCentralManager {
+ internal var centralManagerState: CBCentralManagerState {
+ get {
+ return CBCentralManagerState(rawValue: state.rawValue) ?? .Unknown
+ }
+ }
+}
+
+///
+/// BeaconScanner
+///
+class BeaconScanner: NSObject, CBCentralManagerDelegate {
+ /// Dictionary holding the frames supported by each beacon that we find while scanning
+ var beaconsFound = [CBPeripheral : BeaconFramesSupported]()
+ var connectingTime = 0
+ var scanningTime = 0
+ var gettingBeaconDataTime = 0
+ var scanningTimer: NSTimer?
+ var connectionTimer: NSTimer?
+ var gettingBeaconDataTimer: NSTimer?
+ var lockQueue: dispatch_queue_t
+ var lockQueueForBeaconData: dispatch_queue_t
+ var didFinishScanningCallback: (() -> Void)?
+ var outstandingBeaconRequests: Int = 0
+ var activelyScanning = false
+
+ ///
+ /// When discovering a beacon which broadcasts an EID frame, we need to make a HTTP Get
+ /// request to see if the beacon was registered with the project that the current user
+ /// has selected, which means that the beacon belongs to the user.
+ ///
+ var myEIDBeacons: NSMutableDictionary = [:]
+ private var operationState: OperationState
+ private var centralManager: CBCentralManager!
+ private var connectingPeripheral: CBPeripheral!
+ private let beaconOperationsQueue: dispatch_queue_t =
+ dispatch_queue_create("beacon_operations_queue", nil)
+ private var shouldBeScanning: Bool = false
+ private var GATTOperationCallback:((operationState: OperationState) -> Void)?
+
+ override init() {
+ self.operationState = OperationState.Unknown
+ self.lockQueue = dispatch_queue_create("LockQueue", nil)
+ self.lockQueueForBeaconData = dispatch_queue_create("LockQueueForBeaconData", nil)
+ super.init()
+ self.centralManager = CBCentralManager(delegate: self, queue: self.beaconOperationsQueue)
+ self.centralManager.delegate = self
+ }
+
+ ///
+ /// Start scanning. If Core Bluetooth isn't ready for us just yet, then waits and THEN starts
+ /// scanning.
+ ///
+ func startScanning(completionHandler: () -> Void) {
+ myEIDBeacons.removeAllObjects()
+ didFinishScanningCallback = completionHandler
+ scanningTime = kBeaconScanTime
+ activelyScanning = true
+ scanningTimer = NSTimer.scheduledTimerWithTimeInterval(
+ 1.0,
+ target: self,
+ selector: #selector(BeaconScanner.subtractScanningTime),
+ userInfo: nil,
+ repeats: true)
+ dispatch_async(self.beaconOperationsQueue) {
+ self.startScanningSynchronized()
+ }
+ if EIDConfiguration.projectID != nil {
+ gettingBeaconDataTime = kBeaconDataRequestTimeout
+ gettingBeaconDataTimer = NSTimer.scheduledTimerWithTimeInterval(
+ 1.0,
+ target: self,
+ selector: #selector(BeaconScanner.subtractGettingBeaconDataTime),
+ userInfo: nil,
+ repeats: true)
+ }
+ }
+
+ func subtractScanningTime() {
+ scanningTime -= 1
+ if scanningTime == 0 {
+ self.stopScanning()
+ }
+ }
+
+ func subtractGettingBeaconDataTime() {
+ gettingBeaconDataTime -= 1
+ if gettingBeaconDataTime == 0 {
+ if let currentTimer = gettingBeaconDataTimer {
+ currentTimer.invalidate()
+ gettingBeaconDataTimer = nil
+ }
+ if let callback = didFinishScanningCallback {
+ callback()
+ }
+ }
+ }
+
+ ///
+ /// Stops scanning for Eddystone beacons.
+ ///
+ func stopScanning() {
+ self.centralManager.stopScan()
+ if let currentTimer = scanningTimer {
+ currentTimer.invalidate()
+ }
+ activelyScanning = false
+
+ /// Check if we received all the beacon data by now.
+ var outstandingRequests: Int = 0
+ dispatch_sync(lockQueueForBeaconData) {
+ outstandingRequests = self.outstandingBeaconRequests
+ }
+
+ if (outstandingRequests == 0 || gettingBeaconDataTimer == nil) {
+ if let callback = didFinishScanningCallback {
+ callback()
+ }
+ }
+ }
+
+ ///
+ /// MARK - private methods and delegate callbacks
+ ///
+ func centralManagerDidUpdateState(central: CBCentralManager) {
+ if central.centralManagerState == CBCentralManagerState.PoweredOn && self.shouldBeScanning {
+ self.startScanningSynchronized()
+ }
+ }
+
+ ///
+ /// Core Bluetooth CBCentralManager callback when we discover a beacon. We're not super
+ /// interested in any error situations at this point in time, we only parse each frame
+ /// and save it.
+ ///
+ func centralManager(central: CBCentralManager,
+ didDiscoverPeripheral peripheral: CBPeripheral,
+ advertisementData: [String : AnyObject],
+ RSSI: NSNumber) {
+ if let serviceData = advertisementData[CBAdvertisementDataServiceDataKey]
+ as? [NSObject : AnyObject] {
+ var eft: BeaconInfo.EddystoneFrameType
+ eft = BeaconInfo.frameTypeForFrame(advertisementData)
+ if beaconsFound[peripheral] == nil {
+ let beaconGATTOperations = GATTOperations(peripheral: peripheral)
+ beaconsFound[peripheral] = BeaconFramesSupported(operations: beaconGATTOperations)
+ }
+
+ if eft == BeaconInfo.EddystoneFrameType.TelemetryFrameType {
+ let serviceUUID = CBUUID(string: "FEAA")
+ if let
+ beaconServiceData = serviceData[serviceUUID] as? NSData,
+ (battery, temperature, PDU, time) = BeaconInfo.parseTLMFromFrame(beaconServiceData) {
+ let (days, hours, _) = BeaconInfo.convertDeciseconds(time)
+ let TLM = "\(battery)mV/bit\n\(temperature) °C\n\(PDU)\n\(days) days, \(hours) hours"
+ beaconsFound[peripheral]?.setTLMframe(TLM)
+ }
+ } else if eft == BeaconInfo.EddystoneFrameType.UIDFrameType
+ || eft == BeaconInfo.EddystoneFrameType.EIDFrameType {
+
+ let serviceUUID = CBUUID(string: "FEAA")
+ let _RSSI: Int = RSSI.integerValue
+
+ if let
+ beaconServiceData = serviceData[serviceUUID] as? NSData,
+ beaconInfo =
+ (eft == BeaconInfo.EddystoneFrameType.UIDFrameType
+ ? BeaconInfo.beaconInfoForUIDFrameData(beaconServiceData, RSSI: _RSSI)
+ : BeaconInfo.beaconInfoForEIDFrameData(beaconServiceData, RSSI: _RSSI)) {
+
+ if eft == BeaconInfo.EddystoneFrameType.UIDFrameType {
+ if let beacon = beaconsFound[peripheral] {
+ beacon.setUIDframe(String(beaconInfo.beaconID))
+ }
+ }
+ else if eft == BeaconInfo.EddystoneFrameType.EIDFrameType {
+ beaconsFound[peripheral]?.setEIDframe(String(beaconInfo.beaconID))
+ ///
+ /// If we already have information whether the EID Beacon was registered with the
+ /// selected project, there's no need for us to make another request.
+ ///
+ if myEIDBeacons[String(beaconInfo.beaconID)] == nil {
+ ///
+ /// The user can use the app without being logged in or having a project selected,
+ /// so we will simply not check if the beacon belongs to the user if the user is not
+ /// authenticated.
+ ////
+ if let projectID = EIDConfiguration.projectID {
+ dispatch_sync(lockQueueForBeaconData) {
+ ////
+ /// We want to know how many requestst we made, in order to keep track of how
+ /// many of them return to us.
+ ////
+ self.outstandingBeaconRequests += 1
+ }
+ ///
+ /// We're interested to find if the EID-Beacon that we have was registered
+ /// using the project that is currently selected.
+ ///
+ getBeaconHTTPRequest(String(beaconInfo.beaconID), projectID: projectID)
+ }
+ }
+ }
+ }
+ } else if eft == BeaconInfo.EddystoneFrameType.URLFrameType {
+ let serviceUUID = CBUUID(string: "FEAA")
+
+ if let
+ beaconServiceData = serviceData[serviceUUID] as? NSData,
+ URL = BeaconInfo.parseURLFromFrame(beaconServiceData) {
+ beaconsFound[peripheral]?.setURLframe(String(URL))
+ }
+ } else if eft == BeaconInfo.EddystoneFrameType.EddystoneGATTServiceFrameType {
+ beaconsFound[peripheral]?.setEddystoneGATTServiceFrame()
+ }
+ }
+ }
+
+ func didUpdateState(operationState: OperationState) {
+ self.operationState = operationState
+
+ if let currentTimer = connectionTimer {
+ currentTimer.invalidate()
+ }
+ if operationState == OperationState.Connected {
+ if let beaconGATTOperations = beaconsFound[connectingPeripheral]?.operations {
+ if let callback = GATTOperationCallback {
+ beaconGATTOperations.discoverServices(callback)
+ }
+ }
+ } else if let UICallback = GATTOperationCallback {
+ UICallback(operationState: operationState)
+ }
+ }
+
+ ///
+ /// Attempts to connect to a beacon if the CB Central Manager is ready;
+ /// We set a timer because we assume that if the connection can't be established in that
+ /// amount of time, there's a problem that the user needs to consider.
+ ///
+ func connectToBeacon(peripheral: CBPeripheral,
+ callback: (operationState: OperationState) -> Void) {
+ GATTOperationCallback = callback
+ if self.centralManager.centralManagerState == CBCentralManagerState.PoweredOn {
+ connectingPeripheral = peripheral
+ if peripheral.state != CBPeripheralState.Connected {
+ self.connectToPeripheralWithTimeout(connectingPeripheral)
+ } else {
+ if let beaconGATTOperations = beaconsFound[connectingPeripheral]?.operations {
+ beaconGATTOperations.discoverServices(callback)
+ }
+ }
+ } else {
+ NSLog("CentralManager state is %d, cannot connect", self.centralManager.state.rawValue)
+ didUpdateState(OperationState.CentralManagerConnectionError)
+ }
+ }
+
+ func connectToPeripheralWithTimeout(peripheral: CBPeripheral) {
+ self.setupTimer()
+ centralManager.connectPeripheral(connectingPeripheral, options: nil)
+ }
+
+ func setupTimer() {
+ connectingTime = kBeaconConnectionMaxTimePermitted
+ connectionTimer = NSTimer.scheduledTimerWithTimeInterval(
+ 1.0,
+ target: self,
+ selector: #selector(BeaconScanner.subtractConnectingTime),
+ userInfo: nil,
+ repeats: true)
+ }
+
+ func subtractConnectingTime() {
+ connectingTime -= 1
+ if connectingTime == 0 {
+ centralManager.cancelPeripheralConnection(connectingPeripheral)
+ didUpdateState(OperationState.ConnectionTimeout)
+ }
+ }
+
+ func centralManager(central: CBCentralManager,
+ didFailToConnectPeripheral peripheral: CBPeripheral,
+ error: NSError?) {
+ NSLog("Connecting failed");
+ ///
+ /// Other situations in which the connection attempt fails,
+ /// the timeout being treated separately.
+ ///
+ if operationState != OperationState.ConnectionTimeout {
+ didUpdateState(OperationState.DidFailToConnect)
+ }
+ }
+
+ func centralManager(central: CBCentralManager, didConnectPeripheral peripheral: CBPeripheral) {
+ NSLog("Beacon connected")
+ didUpdateState(OperationState.Connected)
+ }
+
+ func disconnectFromBeacon(peripheral: CBPeripheral) {
+ if self.centralManager.centralManagerState == CBCentralManagerState.PoweredOn {
+ centralManager.cancelPeripheralConnection(peripheral)
+ } else {
+ NSLog("CentralManager state is %d, cannot disconnect", self.centralManager.state.rawValue)
+ }
+ }
+
+ private func startScanningSynchronized() {
+ if self.centralManager.centralManagerState != CBCentralManagerState.PoweredOn {
+ NSLog("CentralManager state is %d, cannot start scan", self.centralManager.state.rawValue)
+ self.shouldBeScanning = true
+ } else {
+ NSLog("Starting to scan for Eddystones")
+ let services = [CBUUID(string: "FEAA")]
+ let options = [CBCentralManagerScanOptionAllowDuplicatesKey : true]
+ self.centralManager.scanForPeripheralsWithServices(services, options: options)
+ }
+ }
+
+ /// Creates the array with all the necessary information to be displayed.
+ func populateBeaconItems() -> [BeaconItem] {
+ var beaconItems: [BeaconItem] = [BeaconItem]()
+ for (beacon, frames) in beaconsFound {
+ let beaconGATTOperations = beaconsFound[beacon]!.operations
+ let beaconItem: BeaconItem = BeaconItem(peripheral: beacon,
+ frames: frames,
+ operations: beaconGATTOperations)
+ if frames.containsUsefulFrames() {
+ beaconItems.append(beaconItem)
+ }
+
+ }
+ return beaconItems
+ }
+
+ /// Checkes whether or not an EID beacon was registered using this projectID.
+ func getBeaconHTTPRequest(EID: String, projectID: String) {
+ if GIDSignIn.sharedInstance().currentUser != nil {
+ let bearer = GIDSignIn.sharedInstance().currentUser.authentication.accessToken
+ let bearerHeader = "Bearer \(bearer)"
+ let server = "\(kGetBeaconsServer)\(kEddystoneEIDCode)!\(EID)?projectId=\(projectID)"
+ let url = NSURL(string: server)
+ let httpHeaders = ["Authorization" : bearerHeader,
+ "Accept" : "application/json"]
+ if let requestURL = url {
+ ///
+ /// The actual response of the request is not important; we're just checking if we are
+ /// able to get the beacon information, which would mean that it was registered using
+ /// the current project.
+ ///
+ HTTPRequest.makeHTTPRequest(requestURL,
+ method: "GET",
+ postBody: nil,
+ requestHeaders: httpHeaders) { statusCode, _, _ in
+ if self.gettingBeaconDataTimer != nil {
+ var outstandingRequests: Int = 0
+ dispatch_sync(self.lockQueueForBeaconData) {
+ self.outstandingBeaconRequests -= 1
+ outstandingRequests = self.outstandingBeaconRequests
+ }
+ dispatch_sync(self.lockQueue) {
+ /// We're not interested in any statusCode other than 200.
+ if statusCode == 200 {
+ self.myEIDBeacons[EID] = true
+ } else {
+ self.myEIDBeacons[EID] = false
+ }
+ }
+ ///
+ /// If we are not currently scanning and we have no active
+ /// requests, it means that we're done and we have all the
+ /// information that we need.
+ ///
+ if outstandingRequests == 0 && !self.activelyScanning {
+ if let
+ currentTimer = self.gettingBeaconDataTimer,
+ callback = self.didFinishScanningCallback {
+ currentTimer.invalidate()
+ self.gettingBeaconDataTimer = nil
+ callback()
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconTableViewCell.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconTableViewCell.swift
new file mode 100644
index 0000000..c2c154f
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/BeaconTableViewCell.swift
@@ -0,0 +1,41 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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 UIKit
+import CoreBluetooth
+
+///
+/// Customizes each cell from the scanned beacons table view in order to display all the information
+/// that can be found without connecting to the beacon.
+///
+class BeaconTableViewCell: UITableViewCell {
+
+ @IBOutlet weak var nameLabel: UILabel!
+ @IBOutlet weak var framesLabel: UITextView!
+ @IBOutlet weak var cellBackgroundView: UIView!
+ @IBOutlet weak var connectableDot: UIView!
+ @IBOutlet weak var framesView: UIView!
+ /// Holds a reference to the beacon in order to be able to connect to it.
+ var beacon: CBPeripheral!
+ var operations: GATTOperations?
+
+ override func awakeFromNib() {
+ super.awakeFromNib()
+ CustomViews.addShadow(cellBackgroundView)
+ }
+
+ override func setSelected(selected: Bool, animated: Bool) {
+ super.setSelected(selected, animated: animated)
+ }
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Beaconfig.xcdatamodeld/.xccurrentversion b/tools/gatt-config/ios/Beaconfig/Beaconfig/Beaconfig.xcdatamodeld/.xccurrentversion
new file mode 100644
index 0000000..7b2749a
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Beaconfig.xcdatamodeld/.xccurrentversion
@@ -0,0 +1,8 @@
+
+
+
+
+ _XCCurrentVersionName
+ Beaconfig.xcdatamodel
+
+
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Beaconfig.xcdatamodeld/Beaconfig.xcdatamodel/contents b/tools/gatt-config/ios/Beaconfig/Beaconfig/Beaconfig.xcdatamodeld/Beaconfig.xcdatamodel/contents
new file mode 100644
index 0000000..193f33c
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Beaconfig.xcdatamodeld/Beaconfig.xcdatamodel/contents
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/BridgingHeader.h b/tools/gatt-config/ios/Beaconfig/Beaconfig/BridgingHeader.h
new file mode 100644
index 0000000..4675f55
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/BridgingHeader.h
@@ -0,0 +1,8 @@
+#ifndef BridgingHeader_h
+#define BridgingHeader_h
+
+#endif
+
+#import
+#import
+#import
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/ConfigureBeaconSlotData.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/ConfigureBeaconSlotData.swift
new file mode 100644
index 0000000..6bb2e37
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/ConfigureBeaconSlotData.swift
@@ -0,0 +1,384 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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 UIKit
+import Foundation
+import CoreBluetooth
+
+let kEIDFrameLength = 14
+///
+/// Beacons need a couple of seconds to generate the correct EID ADV Slot data after writing out to
+/// that slot and to generate the beacon ECDH public key, so we're waiting for them.
+///
+let kWaitingForBeaconTime: Double = 3
+let kECDHLengthKey = 32
+let kEIDLength = 8
+
+///
+/// We need a couple of things both from the beacon and the service and we want to make sure we
+/// gather all of it and that it's correct.
+///
+class EIDRegistrationData {
+ var beaconEcdhPublicKey: String?
+ var serviceEcdhPublicKey: String?
+ var rotationPeriodExponent: NSNumber?
+ var initialClockValue: String?
+ var initialEid: String?
+
+ func isValid() -> Bool {
+ return beaconEcdhPublicKey != nil &&
+ serviceEcdhPublicKey != nil &&
+ rotationPeriodExponent != nil &&
+ initialClockValue != nil &&
+ initialEid != nil
+ }
+}
+
+class ConfigureBeaconSlotData: GATTOperations {
+ var maxSupportedTotalSlots = 0
+ var currentlyUpdatedSlot = 0
+ var slotUpdateData = [NSNumber : [String : NSData]]()
+ var beaconCapabilities: NSDictionary = [:]
+ var callback: (() -> Void)?
+ var isEIDSlot = false
+ var registrationEIDData: EIDRegistrationData?
+ var statusInfoUpdateAlert: UIAlertController?
+
+ enum ConfigurationState {
+ case ReceivedUpdateData
+ case DidSetActiveSlot
+ case ErrorSettingActiveSlot
+ case DidUpdateTxPower
+ case ErrorUpdatingTxPower
+ case DidUpdateAdvInterval
+ case ErrorUpdatingAdvInterval
+ case DidUpdateSlotData
+ case ErrorUpdatingSlotData
+ case UpdatedAllSlots
+ }
+
+ func didUpdateConfigurationState(configurationState: ConfigurationState) {
+ switch configurationState {
+ case .ReceivedUpdateData:
+ setActiveSlot()
+ case .DidSetActiveSlot:
+ updateTxPower()
+ case .DidUpdateTxPower:
+ updateAdvInterval()
+ case .DidUpdateAdvInterval:
+ updateSlotData()
+ case .DidUpdateSlotData:
+ ///
+ /// The EID slot configuration is special and requires some extra steps:
+ /// * reading the exponent, the value and the initial EID from the slot
+ /// * reading the public ECDH Key
+ ///
+ if isEIDSlot {
+ let delayTime = dispatch_time(DISPATCH_TIME_NOW,
+ Int64(kWaitingForBeaconTime * Double(NSEC_PER_SEC)))
+ dispatch_after(delayTime, dispatch_get_main_queue()) {
+ self.readEIDFrame()
+ }
+ isEIDSlot = false
+ } else {
+ currentlyUpdatedSlot += 1; setActiveSlot()
+ }
+ /// We're done!
+ case .UpdatedAllSlots:
+ if let UICallback = callback {
+ UICallback()
+ }
+ ///
+ /// If we have any problem with the update of the current slot, we just try to update the next
+ /// one; there's no point in discarding all changes just because one failed.
+ ///
+ case .ErrorUpdatingSlotData:
+ currentlyUpdatedSlot += 1; setActiveSlot()
+ default:
+ break
+ }
+ }
+
+ func setActiveSlot() {
+ if currentlyUpdatedSlot >= maxSupportedTotalSlots {
+ didUpdateConfigurationState(ConfigurationState.UpdatedAllSlots)
+ } else {
+ if let characteristic = findCharacteristicByID(CharacteristicID.activeSlot.UUID) {
+ let currentSlot = NSData(bytes: ¤tlyUpdatedSlot, length: 1)
+ peripheral.writeValue(currentSlot,
+ forCharacteristic: characteristic,
+ type: CBCharacteristicWriteType.WithResponse)
+ }
+ }
+ }
+
+ override func peripheral(peripheral: CBPeripheral,
+ didWriteValueForCharacteristic characteristic: CBCharacteristic,
+ error: NSError?) {
+
+ if error != nil {
+ switch characteristic.UUID {
+ case CharacteristicID.activeSlot.UUID:
+ didUpdateConfigurationState(ConfigurationState.ErrorSettingActiveSlot)
+ case CharacteristicID.radioTxPower.UUID:
+ didUpdateConfigurationState(ConfigurationState.ErrorUpdatingTxPower)
+ case CharacteristicID.advertisingInterval.UUID:
+ didUpdateConfigurationState(ConfigurationState.ErrorUpdatingAdvInterval)
+ case CharacteristicID.ADVSlotData.UUID:
+ didUpdateConfigurationState(ConfigurationState.ErrorUpdatingSlotData)
+ default:
+ break
+ }
+ } else {
+ switch characteristic.UUID {
+ case CharacteristicID.activeSlot.UUID:
+ didUpdateConfigurationState(ConfigurationState.DidSetActiveSlot)
+ case CharacteristicID.radioTxPower.UUID:
+ didUpdateConfigurationState(ConfigurationState.DidUpdateTxPower)
+ case CharacteristicID.advertisingInterval.UUID:
+ didUpdateConfigurationState(ConfigurationState.DidUpdateAdvInterval)
+ case CharacteristicID.ADVSlotData.UUID:
+ didUpdateConfigurationState(ConfigurationState.DidUpdateSlotData)
+ default:
+ break
+ }
+ }
+ }
+
+ override func peripheral(peripheral: CBPeripheral,
+ didUpdateValueForCharacteristic characteristic: CBCharacteristic,
+ error: NSError?) {
+ NSLog("did update value for characteristic \(characteristic.UUID)")
+ if let identifiedError = error {
+ NSLog("Error reading characteristic: \(identifiedError)")
+ } else {
+ switch characteristic.UUID {
+ case CharacteristicID.ADVSlotData.UUID:
+ parseEIDFrame()
+ case CharacteristicID.publicECDHKey.UUID:
+ didReadPublicECDH()
+ default: break
+ }
+ }
+ }
+
+ func readEIDFrame() {
+ if let characteristic = findCharacteristicByID(CharacteristicID.ADVSlotData.UUID) {
+ peripheral.readValueForCharacteristic(characteristic)
+ }
+ }
+
+ func readPublicECDH() {
+ if let characteristic = findCharacteristicByID(CharacteristicID.publicECDHKey.UUID) {
+ peripheral.readValueForCharacteristic(characteristic)
+ }
+ }
+
+ func didReadPublicECDH() {
+ if let
+ characteristic = findCharacteristicByID(CharacteristicID.publicECDHKey.UUID),
+ value = characteristic.value {
+ // The ECDH public key's length is 32 bytes
+ if value.length == kECDHLengthKey {
+ var publicECDH = [UInt8](count: kECDHLengthKey, repeatedValue: 0)
+ value.getBytes(&publicECDH, length: kECDHLengthKey * sizeof(UInt8))
+ let publicECDHBase64String = StringUtils.convertNSDataToBase64String(value)
+ if let registrationData = registrationEIDData {
+ registrationData.beaconEcdhPublicKey = publicECDHBase64String
+ if registrationData.isValid() {
+ /// If we have all the registration data, we can finally register the beacon.
+ let configureEID = EIDConfiguration()
+ configureEID.registerBeacon(registrationData) { didRegister in
+ if didRegister {
+ self.didUpdateConfigurationState(ConfigurationState.DidUpdateSlotData)
+ } else {
+ self.didUpdateConfigurationState(ConfigurationState.ErrorUpdatingSlotData)
+ }
+ }
+ } else {
+ didUpdateConfigurationState(ConfigurationState.ErrorUpdatingSlotData)
+ }
+ } else {
+ didUpdateConfigurationState(ConfigurationState.ErrorUpdatingSlotData)
+ }
+ }
+ }
+ }
+
+ func parseEIDFrame() {
+ if let
+ characteristic = findCharacteristicByID(CharacteristicID.ADVSlotData.UUID),
+ value = characteristic.value {
+ var frameType: UInt8 = 0
+ value.getBytes(&frameType, length: sizeof(UInt8))
+ if frameType == BeaconInfo.EddystoneEIDFrameTypeID && value.length == kEIDFrameLength {
+ // The EID Slot Data has the following structure:
+ // 1 byte frame type
+ // 1 byte exponent
+ // 4 byte clock value
+ // 8 byte EID
+ var exponent: UInt8 = 0
+ var clockValue: UInt32 = 0
+ var EIDValue = [UInt8](count: kEIDLength, repeatedValue: 0)
+ value.getBytes(&exponent, range: NSMakeRange(1, 1))
+ value.getBytes(&clockValue, range: NSMakeRange(2, 4))
+ value.getBytes(&EIDValue, range: NSMakeRange(6, 8))
+ /// The clock value is in big endian format, so we want to convert it to little endian.
+ clockValue = CFSwapInt32BigToHost(clockValue)
+ if let registrationData = registrationEIDData {
+ registrationData.rotationPeriodExponent = NSNumber(unsignedChar: exponent)
+ let clockValueString = "\(clockValue)"
+ registrationData.initialClockValue = clockValueString
+ let EIDData = NSData(bytes: EIDValue, length: kEIDLength)
+ let EIDBase64String = StringUtils.convertNSDataToBase64String(EIDData)
+ registrationData.initialEid = EIDBase64String
+ }
+
+ /// We're giving the beacon it needs to compute its public ECDH key.
+ let delayTime = dispatch_time(DISPATCH_TIME_NOW,
+ Int64(kWaitingForBeaconTime * Double(NSEC_PER_SEC)))
+ dispatch_after(delayTime, dispatch_get_main_queue()) {
+ self.readPublicECDH()
+ }
+ } else {
+ didUpdateConfigurationState(ConfigurationState.ErrorUpdatingSlotData)
+ }
+ }
+ }
+
+ // TODO: possibly send as callback a list of errors that appeared while configuring slots
+ func beginBeaconConfiguration(beaconCapabilities: NSDictionary,
+ statusInfoAlert: UIAlertController,
+ slotUpdateData: Dictionary >,
+ callback: () -> Void) {
+ self.callback = callback
+ self.maxSupportedTotalSlots = beaconCapabilities[maxSupportedSlotsKey] as! Int
+ self.currentlyUpdatedSlot = 0
+ self.slotUpdateData = slotUpdateData
+ self.beaconCapabilities = beaconCapabilities
+ self.statusInfoUpdateAlert = statusInfoAlert
+ peripheral.delegate = self
+ didUpdateConfigurationState(ConfigurationState.ReceivedUpdateData)
+ }
+
+ func updateTxPower() {
+ var slotNumber = currentlyUpdatedSlot + 1
+ if let perSlotTxPowerSupported = beaconCapabilities[perSlotTxPowerSupportedKey] {
+ if !(perSlotTxPowerSupported as! Bool) {
+ slotNumber = 0
+ }
+ }
+ if let
+ currentSlotData = slotUpdateData[slotNumber],
+ value = currentSlotData[slotDataTxPowerKey],
+ characteristic = findCharacteristicByID(CharacteristicID.radioTxPower.UUID) {
+ var txPower: Int8 = 0
+ value.getBytes(&txPower, length: sizeof(Int8))
+ let val = NSData(bytes: &txPower, length: sizeof(Int8))
+ peripheral.writeValue(val, forCharacteristic: characteristic, type: .WithResponse)
+ } else {
+ updateAdvInterval()
+ }
+ }
+
+ func updateAdvInterval() {
+ var slotNumber = currentlyUpdatedSlot + 1
+ if let perSlotAdvIntervalSupported = beaconCapabilities[perSlotAdvIntervalsSupportedKey] {
+ if !(perSlotAdvIntervalSupported as! Bool) {
+ slotNumber = 0
+ }
+ }
+ if let
+ currentSlotData = slotUpdateData[slotNumber],
+ value = currentSlotData[slotDataAdvIntervalKey],
+ characteristic = findCharacteristicByID(CharacteristicID.advertisingInterval.UUID) {
+ var advInterval: UInt16 = 0
+ value.getBytes(&advInterval, length: sizeof(UInt16))
+ var bigEndianAdvInterv: UInt16 = CFSwapInt16HostToBig(advInterval)
+ let val = NSData(bytes: &bigEndianAdvInterv, length: sizeof(UInt16))
+ peripheral.writeValue(val, forCharacteristic: characteristic, type: .WithResponse)
+ } else {
+ updateSlotData()
+ }
+ }
+
+ ///
+ /// We usually get the slot data in the right format from the moment when we create a dictionary
+ /// with it. There's no need to do more computations.
+ ///
+ func updateSlotData() {
+ isEIDSlot = false
+ let slotNumber = currentlyUpdatedSlot + 1
+ if let
+ currentSlotData = slotUpdateData[slotNumber],
+ value = currentSlotData[slotDataURLKey],
+ characteristic = findCharacteristicByID(CharacteristicID.ADVSlotData.UUID) {
+ peripheral.writeValue(value, forCharacteristic: characteristic, type: .WithResponse)
+ } else if let
+ currentSlotData = slotUpdateData[slotNumber],
+ value = currentSlotData[slotDataUIDKey],
+ characteristic = findCharacteristicByID(CharacteristicID.ADVSlotData.UUID) {
+ peripheral.writeValue(value, forCharacteristic: characteristic, type: .WithResponse)
+ } else if let
+ currentSlotData = slotUpdateData[slotNumber],
+ value = currentSlotData[slotDataTLMKey],
+ characteristic = findCharacteristicByID(CharacteristicID.ADVSlotData.UUID) {
+ peripheral.writeValue(value, forCharacteristic: characteristic, type: .WithResponse)
+ } else if let
+ currentSlotData = slotUpdateData[slotNumber],
+ value = currentSlotData[slotDataNoFrameKey],
+ characteristic = findCharacteristicByID(CharacteristicID.ADVSlotData.UUID) {
+ peripheral.writeValue(value, forCharacteristic: characteristic, type: .WithResponse)
+ } else if let
+ currentSlotData = slotUpdateData[slotNumber],
+ _ = currentSlotData[slotDataEIDKey],
+ characteristic = findCharacteristicByID(CharacteristicID.ADVSlotData.UUID) {
+ ///
+ /// The EID configuration requires additional steps; we don't have the frameData yet and
+ /// we must take it from the server. After writing it, we're still not done with this!
+ ///
+ isEIDSlot = true
+ let configurationEID = EIDConfiguration()
+ configurationEID.getEIDParams() {serviceKey, minRotationExponent, maxRotationExponent in
+ if let
+ key = serviceKey,
+ minExponent = minRotationExponent,
+ _ = maxRotationExponent {
+ let frameData = NSMutableData()
+ frameData.appendBytes([BeaconInfo.EddystoneEIDFrameTypeID], length: sizeof(UInt8))
+ if let
+ serviceKeyData = NSData(base64EncodedString: key,
+ options: NSDataBase64DecodingOptions.IgnoreUnknownCharacters) {
+ frameData.appendData(serviceKeyData)
+ if serviceKeyData.length == kECDHLengthKey {
+ self.registrationEIDData = EIDRegistrationData()
+ self.registrationEIDData?.serviceEcdhPublicKey = key
+ frameData.appendBytes([minExponent], length: sizeof(UInt8))
+ self.peripheral.writeValue(frameData,
+ forCharacteristic: characteristic,
+ type: .WithResponse)
+ } else {
+ self.didUpdateConfigurationState(ConfigurationState.ErrorUpdatingSlotData)
+ }
+ }
+ } else {
+ self.didUpdateConfigurationState(ConfigurationState.ErrorUpdatingSlotData)
+ }
+ }
+ } else {
+ currentlyUpdatedSlot += 1
+ setActiveSlot()
+ }
+ }
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/ConnectionView.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/ConnectionView.swift
new file mode 100644
index 0000000..92a3427
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/ConnectionView.swift
@@ -0,0 +1,323 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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 Foundation
+import UIKit
+
+///
+/// Displays the state of the attempt to connect to the beacon and unlocking beacon options.
+/// The class was created in order to provide details about connection problems and
+/// to allow the user to retry to connect to the beacon.
+///
+class ConnectionView: UIViewController, UITextFieldDelegate, UIGestureRecognizerDelegate {
+ let connectionView = UIView()
+ var stateTextView: UITextView?
+ var retryButtonView: UIView?
+ var holderView: UIView!
+ var unlockHolderView: UIView!
+ var textField: UITextField!
+ var connectionHolderView: UIView!
+ var retryConnectionCallback: (() -> Void)?
+ var unlockBeaconCallback: ((passkey: String) -> Void)?
+ var unlockButton: UIButton!
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ }
+
+ func textField(textField: UITextField,
+ shouldChangeCharactersInRange range: NSRange,
+ replacementString string: String) -> Bool {
+ if string.characters.count == 0 {
+ return true
+ }
+ let currentText = textField.text ?? ""
+ let text = (currentText as NSString).stringByReplacingCharactersInRange(range,
+ withString: string)
+
+ let currentCharacterCount = textField.text?.characters.count ?? 0
+ if (range.length + range.location > currentCharacterCount){
+ return false
+ }
+ let newLength = currentCharacterCount + string.characters.count - range.length
+ var hasRightLength: Bool = false
+ if textField == self.textField {
+ if newLength <= kPassCodeLength {
+ hasRightLength = true
+ }
+ if newLength == kPassCodeLength {
+ unlockButton.enabled = true
+ }
+ }
+ return StringUtils.inHexadecimalString(text) && hasRightLength
+ }
+
+ func textFieldShouldReturn(textField: UITextField) -> Bool {
+ textField.resignFirstResponder()
+ return false
+ }
+
+ func createHolderView(topView: UIView, pinTop: NSLayoutAttribute) -> UIView {
+ let holder: UIView = UIView()
+ connectionView.addSubview(holder)
+ CustomViews.setConstraints(holder,
+ holderView: connectionView,
+ topView: topView,
+ leftView: nil,
+ rightView: nil,
+ height: nil,
+ width: nil,
+ pinTopAttribute: pinTop,
+ setBottomConstraints: false,
+ marginConstraint: kSlotDataMarginConstraint)
+ holder.backgroundColor = UIColor.whiteColor()
+ CustomViews.addShadow(holder)
+ return holder
+ }
+
+
+ func createLabelWithText(text: String, holder: UIView, image: UIImageView) -> UILabel {
+ let textLabel = UILabel()
+ textLabel.text = text
+ textLabel.textColor = kGreenColor
+ textLabel.font = UIFont.systemFontOfSize(19, weight: UIFontWeightSemibold)
+ holder.addSubview(textLabel)
+
+ holder.addSubview(image)
+ CustomViews.setConstraints(image,
+ holderView: holder,
+ topView: holder,
+ leftView: nil,
+ rightView: nil,
+ height: 20,
+ width: 20,
+ pinTopAttribute: .Top,
+ setBottomConstraints: false,
+ marginConstraint: kSlotDataMarginConstraint)
+ CustomViews.setConstraints(textLabel,
+ holderView: holder,
+ topView: holder,
+ leftView: image,
+ rightView: nil,
+ height: kLabelHeight,
+ width: kLabelWidth,
+ pinTopAttribute: .Top,
+ setBottomConstraints: false,
+ marginConstraint: kSlotDataMarginConstraint)
+
+ return textLabel
+ }
+
+ func createTextView(text: String, holder: UIView, topView: UIView) -> UITextView {
+ let textView = UITextView()
+ holder.addSubview(textView)
+ textView.textAlignment = NSTextAlignment.Left
+ textView.text = text
+ textView.editable = false
+ textView.dataDetectorTypes = UIDataDetectorTypes.All
+ textView.textContainer.maximumNumberOfLines = 3
+ textView.scrollEnabled = false
+ CustomViews.setConstraints(textView,
+ holderView: holder,
+ topView: topView,
+ leftView: nil,
+ rightView: nil,
+ height: nil,
+ width: nil,
+ pinTopAttribute: .Bottom,
+ setBottomConstraints: false,
+ marginConstraint: kSlotDataMarginConstraint)
+ textView.textColor = UIColor.darkGrayColor()
+
+ return textView
+ }
+
+ func addRetryButton() {
+ let retryButton = UIButton()
+ retryButtonView?.addSubview(retryButton)
+ CustomViews.setConstraints(retryButton,
+ holderView: retryButtonView!,
+ topView: retryButtonView!,
+ leftView: nil,
+ rightView: retryButtonView,
+ height: 20,
+ width: 100,
+ pinTopAttribute: .Top,
+ setBottomConstraints: true,
+ marginConstraint: kTableViewCellMargin)
+ retryButton.setTitle("RETRY", forState: UIControlState.Normal)
+ retryButton.setTitleColor(kGreenColor, forState: UIControlState.Normal)
+ retryButton.titleLabel!.font = UIFont.systemFontOfSize(14, weight: UIFontWeightSemibold)
+ retryButton.titleLabel!.font = UIFont(name: "Arial-BoldMT", size: kTextFontSize)
+ retryButton.titleLabel?.textAlignment = .Right
+ retryButton.addTarget(self,
+ action: #selector(ConnectionView.retryButtonPressed),
+ forControlEvents: .TouchUpInside)
+ }
+
+ func retryButtonPressed() {
+ if let callback = retryConnectionCallback {
+ callback()
+ }
+ }
+
+ func createConnectionView() {
+ connectionHolderView = createHolderView(connectionView, pinTop: .Top)
+ let connectionImage = UIImage(named: "bluetoothConnection")
+ let connectionImageView = UIImageView(image: connectionImage!)
+ connectionImageView.alpha = 0.5
+ let label = createLabelWithText("Connection",
+ holder: connectionHolderView,
+ image: connectionImageView)
+ stateTextView = createTextView("Attempting to connect to the beacon...",
+ holder: connectionHolderView,
+ topView: label)
+ retryButtonView = UIView()
+ connectionHolderView.addSubview(retryButtonView!)
+ CustomViews.setConstraints(retryButtonView!,
+ holderView: connectionHolderView,
+ topView: stateTextView!,
+ leftView: nil,
+ rightView: nil,
+ height: nil,
+ width: nil,
+ pinTopAttribute: .Bottom,
+ setBottomConstraints: false,
+ marginConstraint: kSlotDataMarginConstraint)
+ let progressView = UIProgressView()
+ connectionHolderView.addSubview(progressView)
+ CustomViews.setConstraints(progressView,
+ holderView: connectionHolderView,
+ topView: retryButtonView!,
+ leftView: nil,
+ rightView: nil,
+ height: nil,
+ width: nil,
+ pinTopAttribute: .Bottom,
+ setBottomConstraints: true,
+ marginConstraint: kSlotDataMarginConstraint)
+ }
+
+ func createUnlockingView() -> UIView {
+ unlockHolderView = createHolderView(connectionHolderView, pinTop: .Bottom)
+ let unlockingImage = UIImage(named: "unlock")
+ let unlockingImageView = UIImageView(image: unlockingImage)
+ unlockingImageView.alpha = 0.5
+ let label = createLabelWithText("Unlock Beacon",
+ holder: unlockHolderView,
+ image: unlockingImageView)
+ stateTextView = createTextView("Enter the key to unlock the beacon.\n" +
+ "The key should be a 32 hex characters string.",
+ holder: unlockHolderView,
+ topView: label)
+ textField = createTextField("Insert unlock code",
+ holderView: unlockHolderView,
+ topView: stateTextView!,
+ leftView: nil,
+ topAttribute: .Bottom)
+ textField.delegate = self
+ createUnlockButton(unlockHolderView, topView: textField)
+ return unlockHolderView
+ }
+
+ func configureView(retryConnectionCallback: () -> Void,
+ unlockBeaconCallback: (passkey: String) -> Void) -> UIView {
+ self.retryConnectionCallback = retryConnectionCallback
+ self.unlockBeaconCallback = unlockBeaconCallback
+ connectionView.frame = view.bounds
+ connectionView.backgroundColor = kLightGrayColor
+ createConnectionView()
+ connectionView.addSubview(connectionHolderView)
+ CustomViews.setConstraints(connectionHolderView,
+ holderView: connectionView,
+ topView: connectionView,
+ leftView: nil,
+ rightView: nil,
+ height: nil,
+ width: nil,
+ pinTopAttribute: .Top,
+ setBottomConstraints: false,
+ marginConstraint: kSlotDataMarginConstraint)
+
+ return connectionView
+ }
+ func dismissKeyboard() {
+ view.endEditing(true)
+ }
+
+ func createTextField(placeholder: String,
+ holderView: UIView,
+ topView: UIView,
+ leftView: UIView?,
+ topAttribute: NSLayoutAttribute) -> UITextField {
+ let textField = UITextField()
+ holderView.addSubview(textField)
+ textField.attributedPlaceholder = NSAttributedString(string:placeholder,
+ attributes:[NSForegroundColorAttributeName:
+ UIColor.grayColor()])
+ CustomViews.setConstraints(textField,
+ holderView: holderView,
+ topView: topView,
+ leftView: leftView,
+ rightView: nil,
+ height: kTextFieldHeight,
+ width: nil,
+ pinTopAttribute: topAttribute,
+ setBottomConstraints: false,
+ marginConstraint: kSlotDataMarginConstraint)
+
+ textField.layer.backgroundColor = UIColor.whiteColor().CGColor
+ textField.layer.borderColor = UIColor.grayColor().CGColor
+ textField.layer.borderWidth = 0.0
+ textField.layer.masksToBounds = false
+ CustomViews.addShadow(textField)
+ textField.font = UIFont(name: "Menlo-Regular", size: kTextFieldFontSize)
+ textField.autocapitalizationType = .None
+ textField.layer.sublayerTransform = CATransform3DMakeTranslation(3, 0, 0)
+ textField.userInteractionEnabled = true
+ textField.enabled = true
+ return textField
+ }
+
+ func createUnlockButton(holder: UIView, topView: UIView) -> UIView {
+ unlockButton = UIButton()
+ holder.addSubview(unlockButton)
+ CustomViews.setConstraints(unlockButton,
+ holderView: holder,
+ topView: topView,
+ leftView: nil,
+ rightView: holder,
+ height: kButtonWidth/5,
+ width: kButtonWidth,
+ pinTopAttribute: .Bottom,
+ setBottomConstraints: true,
+ marginConstraint: kSlotDataMarginConstraint)
+ unlockButton.setTitle("UNLOCK", forState: UIControlState.Normal)
+ unlockButton.setTitleColor(kGreenColor, forState: UIControlState.Normal)
+ unlockButton.setTitleColor(UIColor.darkGrayColor(), forState: UIControlState.Disabled)
+ unlockButton.titleLabel!.font = UIFont(name: "Arial-BoldMT", size: kTextFontSize)
+ unlockButton.titleLabel?.textAlignment = .Right
+ unlockButton.addTarget(self,
+ action: #selector(ConnectionView.unlockButtonPressed),
+ forControlEvents: .TouchUpInside)
+ unlockButton.enabled = false
+ return unlockButton
+ }
+
+ func unlockButtonPressed() {
+ if let callback = unlockBeaconCallback {
+ callback(passkey: textField.text!)
+ }
+ }
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/CustomViews.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/CustomViews.swift
new file mode 100644
index 0000000..ce19b83
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/CustomViews.swift
@@ -0,0 +1,264 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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 UIKit
+import Foundation
+
+let kTableViewCellMargin: CGFloat = 4
+let kFrameDescriptionWidth: CGFloat = 250
+let kFrameTitleWidth: CGFloat = 50
+let kUIDRequiredLength = 32
+let kShadowOpacity: Float = 0.5
+let kShadowOffsetWidth: CGFloat = 0.3
+let kShadowOffsetHeight: CGFloat = 0.7
+let kTitleFontSize: CGFloat = 15
+let kTextFontSize: CGFloat = 14
+let kTextFieldFontSize: CGFloat = 12.5
+let kLabelFontSize: CGFloat = 11
+
+///
+/// Class that enables setting constraints to views and placing them in
+/// certain places of the screen, in relation to other views; this is useful
+/// because it makes layout easier - all views are designed using this class
+/// which saves from a lot of repetitive code.
+///
+class CustomViews {
+ ///
+ /// Places the current view inside the holder view, surrounded by the top, left and right views,
+ /// with a certain margin constraint. In order to establish the size of the holderView,
+ /// if the current view is the last one, then bottom constraints must be set.
+ ///
+ class func setConstraints(view: UIView,
+ holderView: UIView,
+ topView: UIView,
+ leftView: UIView?,
+ rightView: UIView?,
+ height: CGFloat?,
+ width: CGFloat?,
+ pinTopAttribute: NSLayoutAttribute,
+ setBottomConstraints: Bool,
+ marginConstraint: CGFloat) {
+ if rightView == nil {
+ if leftView == nil {
+ let leadingConstraint = NSLayoutConstraint(item: view,
+ attribute:.LeadingMargin,
+ relatedBy: .Equal,
+ toItem: holderView,
+ attribute: .LeadingMargin,
+ multiplier: 1.0,
+ constant: marginConstraint)
+ NSLayoutConstraint.activateConstraints([leadingConstraint])
+ } else {
+ let leadingConstraint = NSLayoutConstraint(item: view,
+ attribute:.LeadingMargin,
+ relatedBy: .Equal,
+ toItem: leftView,
+ attribute: .TrailingMargin,
+ multiplier: 1.0,
+ constant: marginConstraint * 2)
+ NSLayoutConstraint.activateConstraints([leadingConstraint])
+ }
+ }
+
+ if width != nil {
+ let widthConstraint = NSLayoutConstraint(item: view,
+ attribute:.Width,
+ relatedBy: .Equal,
+ toItem: nil,
+ attribute: .NotAnAttribute,
+ multiplier: 1.0,
+ constant: width!)
+ NSLayoutConstraint.activateConstraints([widthConstraint])
+ } else if rightView == nil {
+ let trailingConstraints = NSLayoutConstraint(item: view,
+ attribute:.TrailingMargin,
+ relatedBy: .Equal,
+ toItem: holderView,
+ attribute: .TrailingMargin,
+ multiplier: 1.0,
+ constant: -marginConstraint)
+ NSLayoutConstraint.activateConstraints([trailingConstraints])
+ }
+ if rightView != nil {
+ let marginAttribute: NSLayoutAttribute!
+ if rightView == holderView {
+ marginAttribute = .TrailingMargin
+ } else {
+ marginAttribute = .LeadingMargin
+ }
+ let trailingConstraints = NSLayoutConstraint(item: view,
+ attribute:.TrailingMargin,
+ relatedBy: .Equal,
+ toItem: rightView,
+ attribute: marginAttribute,
+ multiplier: 1.0,
+ constant: -marginConstraint)
+ NSLayoutConstraint.activateConstraints([trailingConstraints])
+ }
+ if height != nil {
+ let heightConstraint = NSLayoutConstraint(item: view,
+ attribute: .Height,
+ relatedBy: .Equal,
+ toItem: nil,
+ attribute: .NotAnAttribute,
+ multiplier: 1.0,
+ constant: height!)
+ NSLayoutConstraint.activateConstraints([heightConstraint])
+ }
+
+ let pinTopConstraints = NSLayoutConstraint(item: view,
+ attribute: .Top,
+ relatedBy: .Equal,
+ toItem: topView,
+ attribute: pinTopAttribute,
+ multiplier: 1.0,
+ constant: marginConstraint)
+ if setBottomConstraints {
+ let bottomConstraints = NSLayoutConstraint(item: view,
+ attribute: .Bottom,
+ relatedBy: .Equal,
+ toItem: holderView,
+ attribute: .Bottom,
+ multiplier: 1.0,
+ constant: -marginConstraint)
+ NSLayoutConstraint.activateConstraints([bottomConstraints])
+ }
+
+ view.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activateConstraints([pinTopConstraints])
+ }
+
+ class func createFrameDisplayView(type: BeaconInfo.EddystoneFrameType,
+ data: String,
+ holder: UIView,
+ topView: UIView,
+ pinTop: NSLayoutAttribute,
+ setBottomConstraints: Bool) -> UIView {
+ var title: String
+ var description: String
+ var dataDescription: String
+ switch type {
+ case .UIDFrameType:
+ title = "UID"
+ description = "Namespace: \nInstance:"
+ dataDescription = getUIDDescription(data)
+ case .URLFrameType:
+ title = "URL"
+ description = "Link:"
+ dataDescription = data
+ case .TelemetryFrameType:
+ title = "TLM"
+ description = "Battery:\nTemperature:\nTime working:\nPDU Count:"
+ dataDescription = data
+ case .EIDFrameType:
+ title = "EID"
+ description = "Ephemeral ID:"
+ dataDescription = data
+ default:
+ return UIView()
+ }
+
+ let titleLabel = UILabel()
+ holder.addSubview(titleLabel)
+ titleLabel.font = UIFont(name: "Arial-BoldMT", size: kTitleFontSize)
+ CustomViews.setConstraints(titleLabel,
+ holderView: holder,
+ topView: topView,
+ leftView: nil,
+ rightView: nil,
+ height: nil,
+ width: kFrameTitleWidth,
+ pinTopAttribute: pinTop,
+ setBottomConstraints: setBottomConstraints,
+ marginConstraint: kTableViewCellMargin)
+ titleLabel.text = title
+ titleLabel.textColor = UIColor.darkGrayColor()
+
+ let infoLabel = UILabel()
+ holder.addSubview(infoLabel)
+ CustomViews.setConstraints(infoLabel,
+ holderView: holder,
+ topView: topView,
+ leftView: titleLabel,
+ rightView: nil,
+ height: nil,
+ width: kFrameDescriptionWidth,
+ pinTopAttribute: pinTop,
+ setBottomConstraints: setBottomConstraints,
+ marginConstraint: kTableViewCellMargin)
+ infoLabel.text = description
+ infoLabel.numberOfLines = 4
+ infoLabel.font = UIFont(name: "Arial", size: kLabelFontSize)
+ infoLabel.textColor = UIColor.darkGrayColor()
+
+ let dataLabel = UILabel()
+ holder.addSubview(dataLabel)
+ CustomViews.setConstraints(dataLabel,
+ holderView: holder,
+ topView: topView,
+ leftView: infoLabel,
+ rightView: holder,
+ height: nil,
+ width: nil,
+ pinTopAttribute: pinTop,
+ setBottomConstraints: setBottomConstraints,
+ marginConstraint: kTableViewCellMargin)
+
+ dataLabel.text = dataDescription
+ dataLabel.textAlignment = .Right
+ dataLabel.numberOfLines = 4
+ dataLabel.font = UIFont(name: "Arial", size: kLabelFontSize)
+ dataLabel.textColor = UIColor.darkGrayColor()
+ return dataLabel
+ }
+
+ class func getUIDDescription(data: String) -> String {
+ var description = ""
+ if data.characters.count == kUIDRequiredLength {
+ ///
+ /// The first 20 characters represent the 10 bytes of namespace,
+ /// the following 12 characters are the 6 bytes of instance.
+ ///
+ let namespace = (data as NSString).substringWithRange(NSRange(location: 0, length: 20))
+ let instance = (data as NSString).substringWithRange(NSRange(location: 20, length: 12))
+ description = "\(namespace)\n\(instance)"
+ }
+ return description
+ }
+
+ class func displayConfigurableBeacon(holder: UIView, topView: UIView, pinTop: NSLayoutAttribute) {
+ let textLabel = UILabel()
+ holder.addSubview(textLabel)
+ textLabel.font = UIFont(name: "Arial", size: kTextFieldFontSize)
+ CustomViews.setConstraints(textLabel,
+ holderView: holder,
+ topView: topView,
+ leftView: nil,
+ rightView: nil,
+ height: nil,
+ width: kFrameDescriptionWidth,
+ pinTopAttribute: pinTop,
+ setBottomConstraints: true,
+ marginConstraint: kTableViewCellMargin)
+ textLabel.text = "Configurable beacon"
+ textLabel.textColor = UIColor.darkGrayColor()
+ }
+
+ class func addShadow(view: UIView) {
+ view.layer.shadowColor = UIColor.darkGrayColor().CGColor
+ view.layer.shadowOpacity = kShadowOpacity
+ view.layer.shadowOffset = CGSize(width: kShadowOffsetWidth, height: kShadowOffsetHeight)
+ view.layer.shadowRadius = 1
+ }
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/EIDConfiguration.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/EIDConfiguration.swift
new file mode 100644
index 0000000..7c90a09
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/EIDConfiguration.swift
@@ -0,0 +1,164 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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 UIKit
+import Foundation
+
+/// We want to wait no more than 10 seconds to wait for the HTTP request to give us an answer
+let kRequestTimeout: Double = 10
+let kGetEIDParamsServer = "https://proximitybeacon.googleapis.com/v1beta1/eidparams"
+let kRegisterBeaconServer = "https://proximitybeacon.googleapis.com/v1beta1/beacons:register"
+let kInstanceLength = 6
+let kNamespaceLength = 10
+
+///
+/// The class implements all the functions that are necessary for configuring the EID and
+/// cannot be included in the regular flow of other frame types configurations,
+/// especially the HTTP requests.
+///
+class EIDConfiguration {
+ ///
+ /// Callback to the general configuration class saying whether or not the registration was
+ /// successful.
+ ///
+ var EIDRegistrationCallback: ((didRegister: Bool) -> Void)?
+
+ ///
+ /// Project ID is needed whenever the user intends to select a new project ID in order to perform
+ /// any EID registrations or beacons scannings (if the user wants to know if any of the beacons
+ /// in the nearby area are related to the currently selected project).
+ ///
+ static var projectID: String?
+
+ func parseEIDParams(data: NSData) -> (String, UInt8, UInt8)? {
+ do {
+ let json = try NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments)
+ if let
+ servicePublicKey = json["serviceEcdhPublicKey"] as? String,
+ minRotationExponent = json["minRotationPeriodExponent"] as? NSNumber,
+ maxRotationExponent = json["maxRotationPeriodExponent"] as? NSNumber {
+ return (servicePublicKey, UInt8(Int(minRotationExponent)), UInt8(Int(maxRotationExponent)))
+ }
+ } catch {
+ print("error serializing JSON: \(error)")
+ }
+ return nil
+ }
+
+ func httpRegisterRequest(beacon: Dictionary ) {
+ let bearer = GIDSignIn.sharedInstance().currentUser.authentication.accessToken
+ let bearerHeader = "Bearer \(bearer)"
+ let server = "\(kRegisterBeaconServer)?projectId=\(EIDConfiguration.projectID!)"
+ let url = NSURL(string: server)
+ let httpHeaders = ["Authorization" : bearerHeader,
+ "Content-Type" : "application/json",
+ "Accept" : "application/json"]
+ var body: NSData
+ do {
+ body = try NSJSONSerialization.dataWithJSONObject(beacon, options: .PrettyPrinted)
+ } catch {
+ print(error)
+ return
+ }
+ if let serverURL = url {
+ HTTPRequest.makeHTTPRequest(serverURL,
+ method: "POST",
+ postBody: body,
+ requestHeaders: httpHeaders) { statusCode, content, error in
+ if let callback = self.EIDRegistrationCallback {
+ if statusCode == 200 {
+ callback(didRegister: true)
+ } else {
+ callback(didRegister: false)
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// We make a request to the server to get the service's public ECDH Key and the minimum
+ /// and maximum accepted values for the exponent.
+ ///
+ func getEIDParams(callback: (key: String?, minExponent:UInt8?, maxExponent: UInt8?) -> Void) {
+ let bearer = GIDSignIn.sharedInstance().currentUser.authentication.accessToken
+ let bearerHeader = "Bearer \(bearer)"
+ let server = kGetEIDParamsServer
+ let url = NSURL(string: server)
+ let httpHeaders = ["Authorization" : bearerHeader,
+ "Accept" : "application/json"]
+ if let serverURL = url {
+ HTTPRequest.makeHTTPRequest(serverURL,
+ method: "GET",
+ postBody: nil,
+ requestHeaders: httpHeaders) { statusCode, content, error in
+ if statusCode == 200 {
+ let (serviceKey, minExp, maxExp) =
+ self.parseEIDParams(content!)!
+ callback(key: serviceKey,
+ minExponent: minExp,
+ maxExponent: maxExp)
+ } else {
+ callback(key: nil, minExponent: nil, maxExponent: nil)
+ }
+ }
+ }
+ }
+
+ func registerBeacon(registrationData: EIDRegistrationData,
+ callback: (didRegister: Bool) -> Void) {
+ EIDRegistrationCallback = callback
+
+ ///
+ /// When registering a beacon that broadcasts Eddystone-EID, we must generate a 16-byte code
+ /// that identifies the beacon; it will only be used for administering the beacon.
+ ///
+ let IDData = generateEddystone16ByteStableID()
+ let IDString = IDData.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))
+ let advertisedID = ["type" : "EDDYSTONE",
+ "id" : IDString]
+ let eidRegistration = ["beaconEcdhPublicKey" : registrationData.beaconEcdhPublicKey!,
+ "serviceEcdhPublicKey" : registrationData.serviceEcdhPublicKey!,
+ "rotationPeriodExponent" : registrationData.rotationPeriodExponent!,
+ "initialClockValue" : registrationData.initialClockValue!,
+ "initialEid" : registrationData.initialEid!]
+ let beacon = ["advertisedId" : advertisedID,
+ "status" : "ACTIVE",
+ "ephemeralIdRegistration" : eidRegistration]
+ httpRegisterRequest(beacon as! Dictionary)
+ }
+
+ /// The namespace is a 10-byte code that is the first part of a MD5 hash of our projectID.
+ func stableIDNamespace() -> NSData {
+ let projectID: String = EIDConfiguration.projectID!
+ let digest = StringUtils.md5(projectID)
+ return digest.subdataWithRange(NSMakeRange(0, kNamespaceLength))
+ }
+
+ ///
+ /// The "stable" Eddystone identifier is composed of:
+ /// - 10 bytes of namespace, which is taken from the MD5 hash of the project ID
+ /// - 6 bytes of instance, which is just a random number.
+ ///
+ func generateEddystone16ByteStableID() -> NSData {
+ let namespace = stableIDNamespace()
+ var instance = [UInt8](count: kInstanceLength, repeatedValue: 0)
+ arc4random_buf(&instance, kInstanceLength)
+ let instanceData = NSData(bytes: instance, length: kInstanceLength)
+ let beaconID = NSMutableData()
+ beaconID.appendData(namespace)
+ beaconID.appendData(instanceData)
+ return beaconID
+ }
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Eddystone.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/Eddystone.swift
new file mode 100644
index 0000000..a4465f5
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Eddystone.swift
@@ -0,0 +1,404 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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 Foundation
+import CoreBluetooth
+
+///
+/// BeaconID
+///
+/// Uniquely identifies an Eddystone compliant beacon.
+///
+class BeaconID : NSObject {
+
+ enum BeaconType {
+ case Eddystone // 10 bytes namespace + 6 bytes instance = 16 byte ID
+ case EddystoneEID // 8 byte ID
+ }
+
+ let beaconType: BeaconType
+
+ ///
+ /// The raw beaconID data. This is typically printed out in hex format.
+ ///
+ let beaconID: [UInt8]
+
+ private init(beaconType: BeaconType!, beaconID: [UInt8]) {
+ self.beaconID = beaconID
+ self.beaconType = beaconType
+ }
+
+ override var description: String {
+ if self.beaconType == BeaconType.Eddystone || self.beaconType == BeaconType.EddystoneEID {
+ let hexid = hexBeaconID(self.beaconID)
+ return "\(hexid)"
+ } else {
+ return "BeaconID with invalid type (\(beaconType))"
+ }
+ }
+
+ private func hexBeaconID(beaconID: [UInt8]) -> String {
+ var retval = ""
+ for byte in beaconID {
+ var s = String(byte, radix:16, uppercase: false)
+ if s.characters.count == 1 {
+ s = "0" + s
+ }
+ retval += s
+ }
+ return retval
+ }
+
+}
+
+func ==(lhs: BeaconID, rhs: BeaconID) -> Bool {
+ if lhs == rhs {
+ return true;
+ } else if lhs.beaconType == rhs.beaconType
+ && rhs.beaconID == rhs.beaconID {
+ return true;
+ }
+
+ return false;
+}
+
+///
+/// BeaconInfo
+///
+/// Contains information fully describing a beacon, including its beaconID, transmission power,
+/// RSSI, and possibly telemetry information.
+///
+class BeaconInfo : NSObject {
+
+ static let EddystoneUIDFrameTypeID: UInt8 = 0x00
+ static let EddystoneURLFrameTypeID: UInt8 = 0x10
+ static let EddystoneTLMFrameTypeID: UInt8 = 0x20
+ static let EddystoneEIDFrameTypeID: UInt8 = 0x30
+ static let GATTServiceID = "A3C87500-8ED3-4BDF-8A39-A01BEBEDE295"
+
+ enum EddystoneFrameType {
+ case UnknownFrameType
+ case UIDFrameType
+ case URLFrameType
+ case TelemetryFrameType
+ case EIDFrameType
+ case EddystoneGATTServiceFrameType
+ case NotSetFrameType
+
+ var description: String {
+ switch self {
+ case .UnknownFrameType:
+ return "Unknown Frame Type"
+ case .UIDFrameType:
+ return "UID Frame"
+ case .URLFrameType:
+ return "URL Frame"
+ case .TelemetryFrameType:
+ return "TLM Frame"
+ case .EIDFrameType:
+ return "EID Frame"
+ case .EddystoneGATTServiceFrameType:
+ return "EddystoneGATTService Frame"
+ case .NotSetFrameType:
+ return "Frame type not set"
+ }
+ }
+ }
+
+ let beaconID: BeaconID
+ let txPower: Int
+ let RSSI: Int
+
+ private init(beaconID: BeaconID, txPower: Int, RSSI: Int) {
+ self.beaconID = beaconID
+ self.txPower = txPower
+ self.RSSI = RSSI
+ }
+
+ class func frameTypeForFrame(advertisementData: [NSObject : AnyObject])
+ -> EddystoneFrameType {
+ if let advertisementFrameList = advertisementData[CBAdvertisementDataServiceDataKey]
+ as? [NSObject : AnyObject] {
+ let uuid = CBUUID(string: "FEAA")
+ if let frameData = advertisementFrameList[uuid] as? NSData {
+ if frameData.length > 1 {
+ let count = frameData.length
+ var frameBytes = [UInt8](count: count, repeatedValue: 0)
+ frameData.getBytes(&frameBytes, length: count)
+
+ if frameBytes[0] == EddystoneUIDFrameTypeID {
+ return EddystoneFrameType.UIDFrameType
+ } else if frameBytes[0] == EddystoneTLMFrameTypeID {
+ return EddystoneFrameType.TelemetryFrameType
+ } else if frameBytes[0] == EddystoneEIDFrameTypeID {
+ return EddystoneFrameType.EIDFrameType
+ } else if frameBytes[0] == EddystoneURLFrameTypeID {
+ return EddystoneFrameType.URLFrameType
+ }
+ } else {
+ if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] {
+ for service in serviceUUIDs as! NSArray {
+ let stringServiceUUID = String(service)
+ if GATTServiceID.compare(stringServiceUUID, options: .CaseInsensitiveSearch) ==
+ NSComparisonResult.OrderedSame {
+ return EddystoneFrameType.EddystoneGATTServiceFrameType
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return EddystoneFrameType.UnknownFrameType
+ }
+
+ class func telemetryDataForFrame(advertisementFrameList: [NSObject : AnyObject]!) -> NSData? {
+ return advertisementFrameList[CBUUID(string: "FEAA")] as? NSData
+ }
+
+ ///
+ /// Unfortunately, this can't be a failable convenience initialiser just yet because of a "bug"
+ /// in the Swift compiler — it can't tear-down partially initialised objects, so we'll have to
+ /// wait until this gets fixed. For now, class method will do.
+ ///
+ class func beaconInfoForUIDFrameData(frameData: NSData, RSSI: Int)
+ -> BeaconInfo? {
+ if frameData.length > 1 {
+ let count = frameData.length
+ var frameBytes = [UInt8](count: count, repeatedValue: 0)
+ frameData.getBytes(&frameBytes, length: count)
+
+ if frameBytes[0] != EddystoneUIDFrameTypeID {
+ NSLog("Unexpected non UID Frame passed to BeaconInfoForUIDFrameData.")
+ return nil
+ } else if frameBytes.count < 18 {
+ NSLog("Frame Data for UID Frame unexpectedly truncated in BeaconInfoForUIDFrameData.")
+ }
+
+ let txPower = Int(Int8(bitPattern:frameBytes[1]))
+ let beaconID: [UInt8] = Array(frameBytes[2..<18])
+ let bid = BeaconID(beaconType: BeaconID.BeaconType.Eddystone, beaconID: beaconID)
+ return BeaconInfo(beaconID: bid, txPower: txPower, RSSI: RSSI)
+ }
+
+ return nil
+ }
+
+ class func beaconInfoForEIDFrameData(frameData: NSData, RSSI: Int)
+ -> BeaconInfo? {
+ if frameData.length > 1 {
+ let count = frameData.length
+ var frameBytes = [UInt8](count: count, repeatedValue: 0)
+ frameData.getBytes(&frameBytes, length: count)
+
+ if frameBytes[0] != EddystoneEIDFrameTypeID {
+ NSLog("Unexpected non EID Frame passed to BeaconInfoForEIDFrameData.")
+ return nil
+ } else if frameBytes.count < 10 {
+ NSLog("Frame Data for EID Frame unexpectedly truncated in BeaconInfoForEIDFrameData.")
+ }
+
+ let txPower = Int(Int8(bitPattern:frameBytes[1]))
+ let beaconID: [UInt8] = Array(frameBytes[2..<10])
+ let bid = BeaconID(beaconType: BeaconID.BeaconType.EddystoneEID, beaconID: beaconID)
+ return BeaconInfo(beaconID: bid, txPower: txPower, RSSI: RSSI)
+ }
+
+ return nil
+ }
+
+ class func parseURLFromFrame(frameData: NSData) -> NSURL? {
+ if frameData.length > 0 {
+ let count = frameData.length
+ var frameBytes = [UInt8](count: count, repeatedValue: 0)
+ frameData.getBytes(&frameBytes, length: count)
+ let frameTypeLength = 2
+ /// Check if the URL was corrupted.
+ if count > frameTypeLength {
+ if let URLPrefix = URLPrefixFromByte(frameBytes[2]) {
+ var output = URLPrefix
+ for i in 3.. (UInt16, Double, UInt32, UInt32)? {
+
+ ///
+ /// The unencrypted telemetry frame has the length of 14 bytes; we want to display information
+ /// in the scan results list, but we're not interested for now in the encrypted telemetry.
+ ///
+ if frameData.length == 14 {
+ var batteryVoltage: UInt16 = 0
+ var beaconTemperature: UInt16 = 0
+ var advPDUCount: UInt32 = 0
+ var timeSinceReboot: UInt32 = 0
+
+ frameData.getBytes(&batteryVoltage, range: NSMakeRange(2, 2))
+ frameData.getBytes(&beaconTemperature, range: NSMakeRange(4, 2))
+ frameData.getBytes(&advPDUCount, range: NSMakeRange(6, 4))
+ frameData.getBytes(&timeSinceReboot, range: NSMakeRange(10, 4))
+ batteryVoltage = CFSwapInt16BigToHost(batteryVoltage)
+ beaconTemperature = CFSwapInt16BigToHost(beaconTemperature)
+ /// Convert temperature from 8.8 fixed point notation.
+ let realBeaconTemperature: Double = Double(beaconTemperature) / 256.0
+ advPDUCount = CFSwapInt32BigToHost(advPDUCount)
+ timeSinceReboot = CFSwapInt32BigToHost(timeSinceReboot)
+ return (batteryVoltage, realBeaconTemperature, advPDUCount, timeSinceReboot)
+ }
+ return nil
+ }
+
+ override var description: String {
+ switch self.beaconID.beaconType {
+ case .Eddystone:
+ return "Eddystone \(self.beaconID), txPower: \(self.txPower), RSSI: \(self.RSSI)"
+ case .EddystoneEID:
+ return "Eddystone EID \(self.beaconID), txPower: \(self.txPower), RSSI: \(self.RSSI)"
+ }
+ }
+
+ class func URLPrefixFromByte(schemeID: UInt8) -> String? {
+ switch schemeID {
+ case 0x00:
+ return "http://www."
+ case 0x01:
+ return "https://www."
+ case 0x02:
+ return "http://"
+ case 0x03:
+ return "https://"
+ default:
+ return nil
+ }
+ }
+
+ class func encodedStringFromByte(charVal: UInt8) -> String? {
+ switch charVal {
+ case 0x00:
+ return ".com/"
+ case 0x01:
+ return ".org/"
+ case 0x02:
+ return ".edu/"
+ case 0x03:
+ return ".net/"
+ case 0x04:
+ return ".info/"
+ case 0x05:
+ return ".biz/"
+ case 0x06:
+ return ".gov/"
+ case 0x07:
+ return ".com"
+ case 0x08:
+ return ".org"
+ case 0x09:
+ return ".edu"
+ case 0x0a:
+ return ".net"
+ case 0x0b:
+ return ".info"
+ case 0x0c:
+ return ".biz"
+ case 0x0d:
+ return ".gov"
+ default:
+ return String(data: NSData(bytes: [ charVal ] as [UInt8], length: 1),
+ encoding: NSUTF8StringEncoding)
+ }
+ }
+
+ ///
+ /// Swift if goofy when working with Strings because characters can't be accessed
+ /// using integer indexes. The function tries to match the beginning of the string
+ /// and encodes it, returning the byte and the truncated string. The function
+ /// is called until the string has no more characters.
+ ///
+ class func byteFromEncodedString(urlText: String) -> (UInt8, String?) {
+ var index = urlText.startIndex
+ if urlText.hasPrefix("http://www.") {
+ index = urlText.startIndex.advancedBy(11)
+ return (0x00, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix("https://www.") {
+ index = urlText.startIndex.advancedBy(12)
+ return (0x01, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix("http://") {
+ index = urlText.startIndex.advancedBy(7)
+ return (0x02, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix("https://") {
+ index = urlText.startIndex.advancedBy(8)
+ return (0x03, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix(".com/") {
+ index = urlText.startIndex.advancedBy(5)
+ return (0x00, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix(".org/") {
+ index = urlText.startIndex.advancedBy(5)
+ return (0x01, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix(".edu/") {
+ index = urlText.startIndex.advancedBy(5)
+ return (0x02, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix(".net/") {
+ index = urlText.startIndex.advancedBy(5)
+ return (0x03, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix(".info/") {
+ index = urlText.startIndex.advancedBy(6)
+ return (0x04, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix(".biz/") {
+ index = urlText.startIndex.advancedBy(5)
+ return (0x05, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix(".gov/") {
+ index = urlText.startIndex.advancedBy(5)
+ return (0x06, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix(".com") {
+ index = urlText.startIndex.advancedBy(4)
+ return (0x07, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix(".org") {
+ index = urlText.startIndex.advancedBy(4)
+ return (0x08, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix(".edu") {
+ index = urlText.startIndex.advancedBy(4)
+ return (0x09, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix(".net") {
+ index = urlText.startIndex.advancedBy(4)
+ return (0x0a, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix(".info") {
+ index = urlText.startIndex.advancedBy(5)
+ return (0x0b, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix(".biz") {
+ index = urlText.startIndex.advancedBy(4)
+ return (0x0c, urlText.substringFromIndex(index))
+ } else if urlText.hasPrefix(".gov") {
+ index = urlText.startIndex.advancedBy(4)
+ return (0x0d, urlText.substringFromIndex(index))
+ }
+ return ([UInt8](String(urlText.characters.first!).utf8)[0],
+ String(urlText.characters.dropFirst()))
+ }
+
+ class func convertDeciseconds(deciseconds: UInt32) -> (Int, Int, Int) {
+ let days: Int = Int(deciseconds) / (24 * 60 * 60 * 10)
+ let hours: Int = Int(deciseconds) / (60 * 60 * 10) - days * 24
+ let minutes: Int = Int(deciseconds / 600) - days * 24 * 60 - hours * 60
+ return (days, hours, minutes)
+ }
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/GATTOperations.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/GATTOperations.swift
new file mode 100644
index 0000000..0e17f3d
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/GATTOperations.swift
@@ -0,0 +1,366 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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 UIKit
+import CoreBluetooth
+
+enum LockState: UInt8 {
+ case Locked = 0
+ case Unlocked
+ case UnlockedPreventAutolock
+ case Unknown = 255
+}
+
+enum CharacteristicID {
+ case capabilities
+ case activeSlot
+ case advertisingInterval
+ case radioTxPower
+ case advertisedTxPower
+ case lockState
+ case unlock
+ case publicECDHKey
+ case EIDIdentityKey
+ case ADVSlotData
+ case factoryReset
+ case remainConnectable
+
+ var UUID: CBUUID {
+ switch self {
+ case .capabilities:
+ return CBUUID(string: "A3C87501-8ED3-4BDF-8A39-A01BEBEDE295")
+ case .activeSlot:
+ return CBUUID(string: "A3C87502-8ED3-4BDF-8A39-A01BEBEDE295")
+ case .advertisingInterval:
+ return CBUUID(string: "A3C87503-8ED3-4BDF-8A39-A01BEBEDE295")
+ case .radioTxPower:
+ return CBUUID(string: "A3C87504-8ED3-4BDF-8A39-A01BEBEDE295")
+ case .advertisedTxPower:
+ return CBUUID(string: "A3C87505-8ED3-4BDF-8A39-A01BEBEDE295")
+ case .lockState:
+ return CBUUID(string: "A3C87506-8ED3-4BDF-8A39-A01BEBEDE295")
+ case .unlock:
+ return CBUUID(string: "A3C87507-8ED3-4BDF-8A39-A01BEBEDE295")
+ case .publicECDHKey:
+ return CBUUID(string: "A3C87508-8ED3-4BDF-8A39-A01BEBEDE295")
+ case .EIDIdentityKey:
+ return CBUUID(string: "A3C87509-8ED3-4BDF-8A39-A01BEBEDE295")
+ case .ADVSlotData:
+ return CBUUID(string: "A3C8750A-8ED3-4BDF-8A39-A01BEBEDE295")
+ case .factoryReset:
+ return CBUUID(string: "A3C8750B-8ED3-4BDF-8A39-A01BEBEDE295")
+ case .remainConnectable:
+ return CBUUID(string: "A3C8750C-8ED3-4BDF-8A39-A01BEBEDE295")
+ }
+ }
+}
+
+let GATTCharacteristics: [CBUUID] = [CharacteristicID.capabilities.UUID,
+ CharacteristicID.activeSlot.UUID,
+ CharacteristicID.advertisingInterval.UUID,
+ CharacteristicID.radioTxPower.UUID,
+ CharacteristicID.advertisedTxPower.UUID,
+ CharacteristicID.lockState.UUID,
+ CharacteristicID.unlock.UUID,
+ CharacteristicID.publicECDHKey.UUID,
+ CharacteristicID.EIDIdentityKey.UUID,
+ CharacteristicID.ADVSlotData.UUID,
+ CharacteristicID.factoryReset.UUID,
+ CharacteristicID.remainConnectable.UUID]
+
+class GATTOperations: NSObject, CBPeripheralDelegate {
+ var peripheral: CBPeripheral!
+ let GATTServiceID = "A3C87500-8ED3-4BDF-8A39-A01BEBEDE295"
+ var gotUnlockChallenge: Bool
+ var unlockChallenge: NSData?
+ var beaconOperationsCallback: ((operationState: OperationState) -> Void)?
+ var lockStateCallback: ((lockState: LockState) -> Void)?
+ var updateLockStateCallback: ((lockState: LockState) -> Void)?
+ var remainConnectableCallback: (() -> Void)?
+ var factoryResetCallback: (() -> Void)?
+ var userPasskey: String?
+ var didAttemptUnlocking = false
+
+ init(peripheral: CBPeripheral) {
+ gotUnlockChallenge = false
+ super.init()
+ self.peripheral = peripheral
+ }
+
+ func didUpdateOperationState(operationState: OperationState) {
+ switch operationState {
+ case OperationState.DidDiscoverServices:
+ discoverCharacteristics()
+ default:
+ if let callback = beaconOperationsCallback {
+ callback(operationState: operationState)
+ }
+ }
+ }
+
+ func discoverServices(callback: (operationState: OperationState) -> Void) {
+ peripheral.delegate = self
+ beaconOperationsCallback = callback
+ if let _ = peripheral.services {
+ /// services were already discovered
+ discoverCharacteristics()
+ } else {
+ let service = CBUUID(string: GATTServiceID)
+ peripheral.discoverServices([service])
+ }
+ }
+
+ func peripheral(peripheral: CBPeripheral,
+ didDiscoverServices error: NSError?) {
+ if let identifiedError = error {
+ NSLog("Error discovering services: \(identifiedError)")
+ didUpdateOperationState(OperationState.DiscoveringServicesError)
+ } else {
+ didUpdateOperationState(OperationState.DidDiscoverServices)
+ }
+ }
+
+ func discoverCharacteristics() {
+ if let services = peripheral.services {
+ for service in services {
+ if service.UUID == CBUUID(string: GATTServiceID) {
+ peripheral.discoverCharacteristics(GATTCharacteristics, forService: service)
+ return
+ }
+ }
+ NSLog("The beacon does not implement the GATT Configuration Service.")
+ didUpdateOperationState(OperationState.NotImplementingGATTConfigService)
+ }
+ }
+
+ func peripheral(peripheral: CBPeripheral,
+ didDiscoverCharacteristicsForService service: CBService,
+ error: NSError?) {
+ if let identifiedError = error {
+ NSLog("Error discovering characteristics: \(identifiedError)")
+ didUpdateOperationState(OperationState.DiscoveringCharacteristicsError)
+ } else {
+ didUpdateOperationState(OperationState.DidDiscoverCharacteristics)
+ }
+ }
+
+ func peripheral(peripheral: CBPeripheral,
+ didWriteValueForCharacteristic characteristic: CBCharacteristic,
+ error: NSError?) {
+
+ print("did write value for characteristic \(characteristic.UUID)")
+ if error != nil {
+ print("there was an error while writing to the characteristis \(characteristic)")
+ }
+ if characteristic.UUID == CharacteristicID.unlock.UUID {
+ if let callback = lockStateCallback {
+ checkLockState(nil, lockStateCallback: callback)
+ }
+ } else if characteristic.UUID == CharacteristicID.lockState.UUID {
+ if let callback = updateLockStateCallback {
+ lockStateCallback = callback
+ getUnlockChallenge()
+ }
+ } else if characteristic.UUID == CharacteristicID.factoryReset.UUID {
+ if let callback = factoryResetCallback {
+ callback()
+ }
+ } else if characteristic.UUID == CharacteristicID.remainConnectable.UUID {
+ if let callback = remainConnectableCallback {
+ callback()
+ }
+ }
+ }
+
+ func peripheral(peripheral: CBPeripheral,
+ didUpdateValueForCharacteristic characteristic: CBCharacteristic,
+ error: NSError?) {
+ print("did update value for characteristic \(characteristic.UUID)")
+ if let identifiedError = error {
+ NSLog("Error reading characteristic: \(identifiedError)")
+ } else {
+ if characteristic.UUID == CharacteristicID.lockState.UUID {
+ parseLockStateValue()
+ } else if characteristic.UUID == CharacteristicID.unlock.UUID {
+ unlockBeacon()
+ }
+ }
+ }
+
+ func parseLockStateValue() {
+ if let
+ characteristic = findCharacteristicByID(CharacteristicID.lockState.UUID),
+ value = characteristic.value {
+ if value.length == 1 {
+ var lockState: UInt8 = 0
+ value.getBytes(&lockState, length: 1)
+ if lockState == LockState.Locked.rawValue {
+ NSLog("The beacon is locked :( .")
+ didUpdateLockState(LockState.Locked)
+ } else {
+ NSLog("The beacon is unlocked!")
+ didUpdateLockState(LockState.Unlocked)
+ }
+ }
+ }
+ }
+
+ func didUpdateLockState(lockState: LockState) {
+ if let callback = lockStateCallback {
+ switch lockState {
+ case LockState.Locked:
+ if userPasskey != nil {
+ if didAttemptUnlocking {
+ callback(lockState: LockState.Locked)
+ } else {
+ getUnlockChallenge()
+ }
+ } else {
+ callback(lockState: LockState.Locked)
+ }
+ case LockState.Unlocked:
+ callback(lockState: LockState.Unlocked)
+ default:
+ callback(lockState: LockState.Unknown)
+ }
+ }
+ }
+
+ func checkLockState(passkey: String?,
+ lockStateCallback: (lockState: LockState) -> Void) {
+ self.lockStateCallback = lockStateCallback
+ if passkey != nil {
+ userPasskey = passkey
+ }
+ if let lockStateCharacteristic = findCharacteristicByID(CharacteristicID.lockState.UUID) {
+ peripheral.readValueForCharacteristic(lockStateCharacteristic)
+ }
+ }
+
+ func findCharacteristicByID(characteristicID: CBUUID) -> CBCharacteristic? {
+ if let services = peripheral.services {
+ for service in services {
+ for characteristic in service.characteristics! {
+ if characteristic.UUID == characteristicID {
+ return characteristic
+ }
+ }
+ }
+ }
+ return nil
+ }
+
+ func beginUnlockingBeacon(passKey: String,
+ lockStateCallback: (lockState: LockState) -> Void) {
+ didAttemptUnlocking = false
+ checkLockState(passKey, lockStateCallback: lockStateCallback)
+
+ }
+
+ func getUnlockChallenge() {
+ if let characteristic = findCharacteristicByID(CharacteristicID.unlock.UUID) {
+ peripheral.readValueForCharacteristic(characteristic)
+ }
+ }
+
+ func unlockBeacon() {
+ if let
+ passKey = userPasskey,
+ characteristic = findCharacteristicByID(CharacteristicID.unlock.UUID),
+ unlockChallenge = characteristic.value {
+ let token: NSData? = AESEncrypt(unlockChallenge, key: passKey)
+ // erase old password
+ userPasskey = nil
+ didAttemptUnlocking = true
+ if let unlockToken = token {
+ peripheral.writeValue(unlockToken,
+ forCharacteristic: characteristic,
+ type: CBCharacteristicWriteType.WithResponse)
+ }
+ }
+ }
+
+ func AESEncrypt(data: NSData, key: String?) -> NSData? {
+ if let passKey = key {
+ let keyBytes = StringUtils.transformStringToByteArray(passKey)
+ let cryptData = NSMutableData(length: Int(data.length) + kCCBlockSizeAES128)!
+ let operation: CCOperation = UInt32(kCCEncrypt)
+ let algoritm: CCAlgorithm = UInt32(kCCAlgorithmAES128)
+ let options: CCOptions = UInt32(kCCOptionECBMode)
+ var numBytesEncrypted :size_t = 0
+ let cryptStatus = CCCrypt(operation,
+ algoritm,
+ options,
+ keyBytes,
+ keyBytes.count,
+ nil,
+ data.bytes,
+ data.length,
+ cryptData.mutableBytes,
+ cryptData.length,
+ &numBytesEncrypted)
+
+ if UInt32(cryptStatus) == UInt32(kCCSuccess) {
+ cryptData.length = Int(numBytesEncrypted)
+ return cryptData as NSData
+ } else {
+ NSLog("Error: \(cryptStatus)")
+ }
+ }
+ return nil
+ }
+
+ func writeNewLockCode(encryptedKey: NSData) {
+ let value = NSMutableData(bytes: [0x00 as UInt8], length: 1)
+ value.appendData(encryptedKey)
+ if let characteristic = findCharacteristicByID(CharacteristicID.lockState.UUID) {
+ peripheral.writeValue(value, forCharacteristic: characteristic, type: .WithResponse)
+ }
+ }
+
+ func changeLockCode(oldCode: String, newCode: String, callback: (lockState: LockState) -> Void) {
+ peripheral.delegate = self
+ updateLockStateCallback = callback
+
+ let newCodeBytes = StringUtils.transformStringToByteArray(newCode)
+ let newCodeData = NSData(bytes: newCodeBytes, length: newCodeBytes.count)
+ let encryptedKey = AESEncrypt(newCodeData, key: oldCode)
+ userPasskey = newCode
+ if let key = encryptedKey {
+ writeNewLockCode(key)
+ }
+ }
+
+ func factoryReset(callback: () -> Void) {
+ factoryResetCallback = callback
+ peripheral.delegate = self
+ if let characteristic = findCharacteristicByID(CharacteristicID.factoryReset.UUID) {
+ let value = NSData(bytes: [0x0B as UInt8], length: 1)
+ peripheral.writeValue(value, forCharacteristic: characteristic, type: .WithResponse)
+ }
+ }
+
+ func changeRemainConnectableState(on: Bool, callback: () -> Void) {
+ remainConnectableCallback = callback
+ peripheral.delegate = self
+ if let characteristic = findCharacteristicByID(CharacteristicID.remainConnectable.UUID) {
+ var value: UInt8 = 0
+ if on {
+ value = 1
+ }
+ let data = NSData(bytes: [value], length: 1)
+ peripheral.writeValue(data, forCharacteristic: characteristic, type: .WithResponse)
+ }
+ }
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/HTTPRequest.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/HTTPRequest.swift
new file mode 100644
index 0000000..ad63992
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/HTTPRequest.swift
@@ -0,0 +1,57 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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 Foundation
+import UIKit
+
+///
+/// Having a class that makes HTTP requests and sends results using callbacks seemes like a clean
+/// implementation, since there are several classes that need to make HTTP requests.
+///
+class HTTPRequest {
+ class func makeHTTPRequest(url: NSURL,
+ method: String,
+ postBody: NSData?,
+ requestHeaders: NSDictionary,
+ completionHandler: (httpResponseCode: Int,
+ response: NSData?,
+ error: NSError?) -> Void) {
+ let request = NSMutableURLRequest(URL: url)
+ for (key, value) in requestHeaders {
+ request.addValue(value as! String, forHTTPHeaderField: key as! String)
+ }
+ request.HTTPMethod = method
+ if let body = postBody {
+ request.HTTPBody = body
+ }
+ let config = NSURLSessionConfiguration.defaultSessionConfiguration()
+ config.timeoutIntervalForRequest = kRequestTimeout
+ let session = NSURLSession(configuration: config)
+ let task = session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in
+ let content = NSString(data: data!, encoding: NSUTF8StringEncoding)
+ NSLog("CONTENT: \(content!)")
+ NSLog("RESPONSE: \(response!)")
+ NSLog("ERROR: \(error)")
+
+ if let urlHTTPResponse = response as? NSHTTPURLResponse {
+ let statusCode = urlHTTPResponse.statusCode
+ completionHandler(httpResponseCode: statusCode, response: data, error: error)
+ } else {
+ completionHandler(httpResponseCode: -1, response: nil, error: error)
+ }
+ });
+ task.resume()
+ }
+
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/HorizontalScrollButtonList.swift b/tools/gatt-config/ios/Beaconfig/Beaconfig/HorizontalScrollButtonList.swift
new file mode 100644
index 0000000..52ccd54
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/HorizontalScrollButtonList.swift
@@ -0,0 +1,185 @@
+// Copyright 2016 Google Inc. All rights reserved.
+//
+// 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.
+
+let globalButtonIndex = 0
+
+import UIKit
+
+///
+/// Displays a custom made horizontal scroll button list, which appears on top of the view.
+/// There can be only one selected button at a time and the class deselects the previously
+/// selected button. The first button to be pressed is the Global one, since the Global view is
+/// the default view to be displayed.
+///
+class HorizontalScrollButtonList: UIView {
+
+ /// Keeps track of the previous slelected button in order to be able to deselect it
+ var previousButtonPressed: UIButton?
+
+ /// Displays a line under the selected button
+ var previousUnderlineView: UIView?
+ var linesArray: [UIView] = []
+ var buttonsArray: [UIButton] = []
+
+ ///
+ /// Callback to the main class in order for it to handle displaying a different view
+ /// for each one of the buttons
+ ///
+ var buttonPressedCallback: ((buttonNumber: Int) -> Void)?
+
+ required init?(coder aDecoder: NSCoder) {
+ super.init(coder: aDecoder)
+ }
+
+ init(buttonSize:CGSize, buttonNames: NSMutableArray, callback: (buttonNumber: Int) -> Void) {
+ buttonPressedCallback = callback
+ super.init(frame: CGRectMake(0, 0, 0, 0))
+ let buttonCount = buttonNames.count
+
+ self.frame.origin = CGPointMake(0,0)
+ self.frame.size.width = buttonSize.width * CGFloat(buttonCount)
+ self.frame.size.height = buttonSize.height
+ self.backgroundColor = UIColor.whiteColor()
+
+ configureLine(buttonSize.width * CGFloat(buttonCount))
+
+ var buttonPosition = CGPointMake(0, 0)
+ let padding: CGFloat = 1
+ let buttonIncrement = buttonSize.width + padding
+
+ for i in 0.. UIButton {
+ let button = UIButton(type: .Custom)
+ button.frame.size = buttonSize
+ button.frame.origin = buttonPosition
+ button.setTitle(title, forState: UIControlState.Normal)
+ button.setTitleColor(UIColor.darkGrayColor(), forState: UIControlState.Normal)
+ button.setTitleColor(UIColor.grayColor(), forState: UIControlState.Selected)
+ button.titleLabel?.font = UIFont(name: "Arial-BoldMT", size: kTextFontSize)
+ button.addTarget(self,
+ action: #selector(HorizontalScrollButtonList.slotButtonPressed(_:)),
+ forControlEvents: .TouchUpInside)
+ button.backgroundColor = UIColor.whiteColor()
+
+ buttonsArray.append(button)
+ self.addSubview(button)
+
+ return button
+ }
+
+ func configureButtonUnderlineView(buttonSize: CGSize, buttonPosition: CGPoint) -> UIView {
+ let view = UIView()
+ let lineHeight: CGFloat = 3
+ view.frame.size = CGSizeMake(buttonSize.width, lineHeight)
+ view.frame.origin = CGPointMake(buttonPosition.x, buttonPosition.y + buttonSize.height)
+ view.backgroundColor = kGreenColor
+ view.hidden = true
+ linesArray.append(view)
+
+ return view
+ }
+
+ func configureLine(width: CGFloat) -> UIView {
+ let line = UIView()
+ line.backgroundColor = UIColor.whiteColor()
+ line.frame.size.height = 2
+ line.frame.size.width = width
+ line.frame.origin = CGPointMake(0, self.frame.height)
+ CustomViews.addShadow(line)
+ self.addSubview(line)
+
+ return line
+ }
+
+ ///
+ /// Handle the swipe motions between the views, changing the selected button with
+ /// the one that coresponds to the view the user is currently seeing.
+ ///
+ func swipeLeft() {
+ if previousButtonPressed != buttonsArray[buttonsArray.count - 1] {
+ for i in 0 ..< buttonsArray.count {
+ if previousButtonPressed == buttonsArray[i] {
+ slotButtonPressed(buttonsArray[i + 1])
+ break
+ }
+ }
+ }
+ }
+
+ func swipeRight() {
+ if previousButtonPressed != buttonsArray[globalButtonIndex] {
+ for i in 0 ..< buttonsArray.count {
+ if previousButtonPressed == buttonsArray[i] {
+ slotButtonPressed(buttonsArray[i - 1])
+ break
+ }
+ }
+ }
+ }
+
+ func slotButtonPressed(sender: UIButton) {
+ if previousButtonPressed != nil {
+ previousButtonPressed!.selected = false
+ previousUnderlineView!.hidden = true
+ }
+ sender.selected = true
+ if let
+ character = sender.titleLabel?.text?.characters.last,
+ buttonNumber = Int(String(character)),
+ callback = buttonPressedCallback {
+ linesArray[buttonNumber].hidden = false
+ previousUnderlineView = linesArray[buttonNumber]
+ callback(buttonNumber: buttonNumber)
+ } else if let callback = buttonPressedCallback {
+ linesArray[globalButtonIndex].hidden = false
+ previousUnderlineView = linesArray[globalButtonIndex]
+ callback(buttonNumber: globalButtonIndex)
+ }
+ previousButtonPressed = sender
+ }
+}
diff --git a/tools/gatt-config/ios/Beaconfig/Beaconfig/Info.plist b/tools/gatt-config/ios/Beaconfig/Beaconfig/Info.plist
new file mode 100644
index 0000000..b393ba8
--- /dev/null
+++ b/tools/gatt-config/ios/Beaconfig/Beaconfig/Info.plist
@@ -0,0 +1,51 @@
+
+
+
+
+