[flicbutton] Initial contribution FlicButton Binding (#9234)

* [flicbutton] Initial contribution FlicButton Binding

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Add config parameter address for FlicButton thing

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Run spotless

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Code cleanup & docs improvement

Signed-off-by: Patrick Fink <mail@pfink.de>

* Apply suggestions from code review

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>

* [flicbutton] Update LICENSE

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Apply suggestions from code review (2) & update to 3.1-SNAPSHOT

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Apply suggestions from code review (3) & fix offline status

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Fix 3rd party source for proper IDE integration

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Simplify config parsing

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Move everything to internal package

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Remove hyphens from port parameter docs example

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Change maintainer to openHAB project

Signed-off-by: Patrick Fink <mail@pfink.de>

* Apply docs suggestions + update to 3.2.0-SNAPSHOT

Signed-off-by: Patrick Fink <mail@pfink.de>

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* [flicbutton] Fix bridge offline & reconnect handling

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Close open socket on dispose

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Improve exception error message in ThingStatus

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Fix README title

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Improve exception error message in ThingStatus

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Style fixes

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Use trace log level for button clicks & status changes

Signed-off-by: Patrick Fink <mail@pfink.de>

* Apply doc improvements from code review

Signed-off-by: Patrick Fink <mail@pfink.de>

Co-authored-by: Matthew Skinner <matt@pcmus.com>

* [flicbutton] Add binding to bom/openhab-addons

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Cleanup / remove guava leftover

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Remove online status description

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Improve flicd hostname label

Signed-off-by: Patrick Fink <mail@pfink.de>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>

* [flicbutton] Do not catch IllegalArgumentException anymore as its not neeed

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Use debug log level instead of info

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Update version and license

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Fix SAT warnings, e.g. add null handling annotations

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Fix SAT warnings (2)

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Concurrency refactoring & fixes

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Cancel initialization task also when already running

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Add javadoc and move FLIC_OPENHAB_EVENT_TRIGGER_MAP constant to constants class

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Use ThingStatusDetail.OFFLINE.GONE when Flic button was removed from bridge

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Fix FlicSimpleclientDiscoveryServiceImpl javadoc

Signed-off-by: Patrick Fink <mail@pfink.de>

* [flicbutton] Fix required definition of thing types

Signed-off-by: Patrick Fink <mail@pfink.de>

Co-authored-by: Fabian Wolter <github@fabian-wolter.de>
Co-authored-by: Matthew Skinner <matt@pcmus.com>
This commit is contained in:
Patrick Fink
2022-02-20 21:53:30 +01:00
committed by GitHub
parent 51bbd34cd7
commit 6c104e241a
42 changed files with 3149 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

View File

@@ -0,0 +1,45 @@
package io.flic.fliclib.javaclient;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Battery status listener.
*
* Add this listener to a {@link FlicClient} by executing {@link FlicClient#addBatteryStatusListener(BatteryStatusListener)}.
*/
public class BatteryStatusListener {
private static AtomicInteger nextId = new AtomicInteger();
int listenerId = nextId.getAndIncrement();
private Bdaddr bdaddr;
Callbacks callbacks;
public BatteryStatusListener(Bdaddr bdaddr, Callbacks callbacks) {
if (bdaddr == null) {
throw new IllegalArgumentException("bdaddr is null");
}
if (callbacks == null) {
throw new IllegalArgumentException("callbacks is null");
}
this.bdaddr = bdaddr;
this.callbacks = callbacks;
}
public Bdaddr getBdaddr() {
return bdaddr;
}
public abstract static class Callbacks {
/**
* This will be called when the battery status has been updated.
* It will also be called immediately after the battery status listener has been created.
* If the button stays connected, this method will be called approximately every three hours.
*
* @param bdaddr Bluetooth device address
* @param batteryPercentage A number between 0 and 100 for the battery level. Will be -1 if unknown.
* @param timestamp Standard UNIX timestamp, in seconds, for the event.
*/
public abstract void onBatteryStatus(Bdaddr bdaddr, int batteryPercentage, long timestamp) throws IOException;
}
}

View File

@@ -0,0 +1,68 @@
package io.flic.fliclib.javaclient;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
/**
* Bluetooth address.
*/
public class Bdaddr {
private byte[] bytes;
/**
* Creates a Bdaddr given the bluetooth address in string format.
*
* @param addr address of the format xx:xx:xx:xx:xx:xx
*/
public Bdaddr(String addr) {
bytes = new byte[6];
bytes[5] = (byte)Integer.parseInt(addr.substring(0, 2), 16);
bytes[4] = (byte)Integer.parseInt(addr.substring(3, 5), 16);
bytes[3] = (byte)Integer.parseInt(addr.substring(6, 8), 16);
bytes[2] = (byte)Integer.parseInt(addr.substring(9, 11), 16);
bytes[1] = (byte)Integer.parseInt(addr.substring(12, 14), 16);
bytes[0] = (byte)Integer.parseInt(addr.substring(15, 17), 16);
}
Bdaddr(InputStream stream) throws IOException {
bytes = new byte[6];
for (int i = 0; i < 6; i++) {
bytes[i] = (byte)stream.read();
}
}
byte[] getBytes() {
return bytes.clone();
}
/**
* Create a string representing the bluetooth address.
*
* @return A string of the bdaddr
*/
@Override
public String toString() {
return String.format("%02x:%02x:%02x:%02x:%02x:%02x", bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]);
}
@Override
public int hashCode() {
return (bytes[0] & 0xff) ^ ((bytes[1] & 0xff) << 8) ^ ((bytes[2] & 0xff) << 16) ^ ((bytes[3] & 0xff) << 24) ^ (bytes[4] & 0xff) ^ ((bytes[5] & 0xff) << 8);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!(obj instanceof Bdaddr)) {
return false;
}
Bdaddr other = (Bdaddr)obj;
return Arrays.equals(bytes, other.bytes);
}
}

View File

@@ -0,0 +1,188 @@
package io.flic.fliclib.javaclient;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import io.flic.fliclib.javaclient.enums.*;
/**
* Button connection channel.
*
* Add this button connection channel to a {@link FlicClient} by executing {@link FlicClient#addConnectionChannel(ButtonConnectionChannel)}.
* You may only have this connection channel added to one {@link FlicClient} at a time.
*/
public class ButtonConnectionChannel {
private static AtomicInteger nextId = new AtomicInteger();
int connId = nextId.getAndIncrement();
FlicClient client;
private Bdaddr bdaddr;
private LatencyMode latencyMode;
private short autoDisconnectTime;
Callbacks callbacks;
final Object lock = new Object();
/**
* Create a connection channel using the specified parameters.
*
* Add this button connection channel to a {@link FlicClient} by executing {@link FlicClient#addConnectionChannel(ButtonConnectionChannel)}.
*
* @param bdaddr
* @param latencyMode
* @param autoDisconnectTime Number of seconds (0 - 511) until disconnect if no button event happens. 512 disables this feature.
* @param callbacks
*/
public ButtonConnectionChannel(Bdaddr bdaddr, LatencyMode latencyMode, short autoDisconnectTime, Callbacks callbacks) {
if (bdaddr == null) {
throw new IllegalArgumentException("bdaddr is null");
}
if (latencyMode == null) {
throw new IllegalArgumentException("latencyMode is null");
}
if (callbacks == null) {
throw new IllegalArgumentException("callbacks is null");
}
this.bdaddr = bdaddr;
this.latencyMode = latencyMode;
this.autoDisconnectTime = autoDisconnectTime;
this.callbacks = callbacks;
}
/**
* Create a connection channel using the specified parameters.
*
* Add this button connection channel to a {@link FlicClient} by executing {@link FlicClient#addConnectionChannel(ButtonConnectionChannel)}.
*
* @param bdaddr
* @param callbacks
*/
public ButtonConnectionChannel(Bdaddr bdaddr, Callbacks callbacks) {
this(bdaddr, LatencyMode.NormalLatency, (short)0x1ff, callbacks);
}
/**
* Get the {@link FlicClient} for this {@link ButtonConnectionChannel}.
*
* @return
*/
public FlicClient getFlicClient() {
return client;
}
public Bdaddr getBdaddr() {
return bdaddr;
}
public LatencyMode getLatencyMode() {
return latencyMode;
}
public short getAutoDisconnectTime() {
return autoDisconnectTime;
}
/**
* Applies new latency mode parameter.
*
* @param latencyMode
*/
public void setLatencyMode(LatencyMode latencyMode) throws IOException {
if (latencyMode == null) {
throw new IllegalArgumentException("latencyMode is null");
}
synchronized (lock) {
this.latencyMode = latencyMode;
FlicClient cl = client;
if (cl != null) {
CmdChangeModeParameters pkt = new CmdChangeModeParameters();
pkt.connId = connId;
pkt.latencyMode = latencyMode;
pkt.autoDisconnectTime = autoDisconnectTime;
cl.sendPacket(pkt);
}
}
}
/**
* Applies new auto disconnect time parameter.
*
* @param autoDisconnectTime Number of seconds (0 - 511) until disconnect if no button event happens. 512 disables this feature.
*/
public void setAutoDisconnectTime(short autoDisconnectTime) throws IOException {
if (latencyMode == null) {
throw new IllegalArgumentException("latencyMode is null");
}
synchronized (lock) {
this.autoDisconnectTime = autoDisconnectTime;
FlicClient cl = client;
if (cl != null) {
CmdChangeModeParameters pkt = new CmdChangeModeParameters();
pkt.connId = connId;
pkt.latencyMode = latencyMode;
pkt.autoDisconnectTime = autoDisconnectTime;
cl.sendPacket(pkt);
}
}
}
/**
* User callbacks for incoming events.
*
* See the protocol specification for further details.
*/
public abstract static class Callbacks {
/**
* Called when the server has received the create connection channel command.
*
* If createConnectionChannelError is {@link CreateConnectionChannelError#NoError}, other events will arrive until {@link #onRemoved} is received.
* There will be no {@link #onRemoved} if an error occurred.
*
* @param channel
* @param createConnectionChannelError
* @param connectionStatus
* @throws IOException
*/
public void onCreateConnectionChannelResponse(ButtonConnectionChannel channel, CreateConnectionChannelError createConnectionChannelError, ConnectionStatus connectionStatus) throws IOException {
}
/**
* Called when the connection channel has been removed.
*
* Check the removedReason to find out why. From this point, the connection channel can be re-added again if you wish.
*
* @param channel
* @param removedReason
* @throws IOException
*/
public void onRemoved(ButtonConnectionChannel channel, RemovedReason removedReason) throws IOException {
}
/**
* Called when the connection status changes.
*
* @param channel
* @param connectionStatus
* @param disconnectReason Only valid if connectionStatus is {@link ConnectionStatus#Disconnected}
* @throws IOException
*/
public void onConnectionStatusChanged(ButtonConnectionChannel channel, ConnectionStatus connectionStatus, DisconnectReason disconnectReason) throws IOException {
}
public void onButtonUpOrDown(ButtonConnectionChannel channel, ClickType clickType, boolean wasQueued, int timeDiff) throws IOException {
}
public void onButtonClickOrHold(ButtonConnectionChannel channel, ClickType clickType, boolean wasQueued, int timeDiff) throws IOException {
}
public void onButtonSingleOrDoubleClick(ButtonConnectionChannel channel, ClickType clickType, boolean wasQueued, int timeDiff) throws IOException {
}
public void onButtonSingleOrDoubleClickOrHold(ButtonConnectionChannel channel, ClickType clickType, boolean wasQueued, int timeDiff) throws IOException {
}
}
}

View File

@@ -0,0 +1,28 @@
package io.flic.fliclib.javaclient;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Button scanner class.
*
* Inherit this class and override the {@link #onAdvertisementPacket(Bdaddr, String, int, boolean, boolean)} method.
* Then add this button scanner to a {@link FlicClient} using {@link FlicClient#addScanner(ButtonScanner)} to start it.
*/
public abstract class ButtonScanner {
private static AtomicInteger nextId = new AtomicInteger();
int scanId = nextId.getAndIncrement();
/**
* This will be called for every received advertisement packet from a Flic button.
*
* @param bdaddr Bluetooth address
* @param name Advertising name
* @param rssi RSSI value in dBm
* @param isPrivate The button is private and won't accept new connections from non-bonded clients
* @param alreadyVerified The server has already verified this button, which means you can connect to it even if it's private
* @param alreadyConnectedToThisDevice The button is already connected to this device
* @param alreadyConnectedToOtherDevice The button is already connected to another device
*/
public abstract void onAdvertisementPacket(Bdaddr bdaddr, String name, int rssi, boolean isPrivate, boolean alreadyVerified, boolean alreadyConnectedToThisDevice, boolean alreadyConnectedToOtherDevice) throws IOException;
}

View File

@@ -0,0 +1,630 @@
package io.flic.fliclib.javaclient;
import io.flic.fliclib.javaclient.enums.CreateConnectionChannelError;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.ArrayDeque;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentSkipListMap;
/**
* Implements a FlicClient over a TCP Socket.
*
* When this class is constructed, a socket connection is established.
*
* You may then send commands to the server and set timers.
*
* Once you are ready with the initialization you must call the {@link #handleEvents()} method which is a main loop that never exits, unless the socket is closed.
*
* For a more detailed description of all commands, events and enums, check the protocol specification.
*/
public class FlicClient {
private Socket socket;
private InputStream socketInputStream;
private OutputStream socketOutputStream;
private ConcurrentHashMap<Integer, ButtonScanner> scanners = new ConcurrentHashMap<>();
private ConcurrentHashMap<Integer, ButtonConnectionChannel> connectionChannels = new ConcurrentHashMap<>();
private ConcurrentHashMap<Integer, ScanWizard> scanWizards = new ConcurrentHashMap<>();
private ConcurrentHashMap<Integer, BatteryStatusListener> batteryStatusListeners = new ConcurrentHashMap<>();
private ConcurrentLinkedQueue<GetInfoResponseCallback> getInfoResponseCallbackQueue = new ConcurrentLinkedQueue<>();
private ArrayDeque<GetButtonInfoResponseCallback> getButtonInfoResponseCallbackQueue = new ArrayDeque<>();
private volatile GeneralCallbacks generalCallbacks = new GeneralCallbacks();
private ConcurrentSkipListMap<Long, TimerTask> timers = new ConcurrentSkipListMap<>();
private Thread handleEventsThread;
/**
* Create a FlicClient and connect to the specified hostName and TCP port
*
* @param hostName
* @param port
* @throws UnknownHostException
* @throws IOException
*/
public FlicClient(String hostName, int port) throws UnknownHostException, IOException {
socket = new Socket(hostName, port);
socket.setKeepAlive(true);
socketInputStream = socket.getInputStream();
socketOutputStream = socket.getOutputStream();
}
/**
* Create a FlicClient and connect to the specified hostName using the default TCP port
*
* @param hostName
* @throws UnknownHostException
* @throws IOException
*/
public FlicClient(String hostName) throws UnknownHostException, IOException {
this(hostName, 5551);
}
/**
* Close the socket.
*
* From this point any use of this FlicClient is illegal.
* The {@link #handleEvents()} will return as soon as the closing is done.
*
* @throws IOException
*/
public void close() throws IOException {
runOnHandleEventsThread(new TimerTask() {
@Override
public void run() throws IOException {
socket.close();
}
});
}
/**
* Set general callbacks to be called upon receiving some specific events.
*
* @param callbacks
*/
public void setGeneralCallbacks(GeneralCallbacks callbacks) {
if (callbacks == null) {
callbacks = new GeneralCallbacks();
}
generalCallbacks = callbacks;
}
/**
* Get info about the current state of the server.
*
* The server will send back its information directly and the callback will be called once the response arrives.
*
* @param callback
* @throws IOException
*/
public void getInfo(GetInfoResponseCallback callback) throws IOException {
if (callback == null) {
throw new IllegalArgumentException("callback is null");
}
getInfoResponseCallbackQueue.add(callback);
CmdGetInfo pkt = new CmdGetInfo();
sendPacket(pkt);
}
/**
* Get button info for a verified button.
*
* The server will send back its information directly and the callback will be called once the response arrives.
* Responses will arrive in the same order as requested.
*
* If the button isn't verified, the data sent to callback will be null.
*
* @param bdaddr The bluetooth address.
* @param callback Callback for the response.
* @throws IOException
*/
public void getButtonInfo(final Bdaddr bdaddr, final GetButtonInfoResponseCallback callback) throws IOException {
if (callback == null) {
throw new IllegalArgumentException("callback is null");
}
// Run on events thread to ensure ordering if multiple requests are issued at the same time
runOnHandleEventsThread(new TimerTask() {
@Override
public void run() throws IOException {
getButtonInfoResponseCallbackQueue.add(callback);
CmdGetButtonInfo pkt = new CmdGetButtonInfo();
pkt.bdaddr = bdaddr;
sendPacket(pkt);
}
});
}
/**
* Add a scanner.
*
* The scan will start directly once the scanner is added.
*
* @param buttonScanner
* @throws IOException
*/
public void addScanner(ButtonScanner buttonScanner) throws IOException {
if (buttonScanner == null) {
throw new IllegalArgumentException("buttonScanner is null");
}
if (scanners.putIfAbsent(buttonScanner.scanId, buttonScanner) != null) {
throw new IllegalArgumentException("Button scanner already added");
}
CmdCreateScanner pkt = new CmdCreateScanner();
pkt.scanId = buttonScanner.scanId;
sendPacket(pkt);
}
/**
* Remove a scanner.
*
* @param buttonScanner The same scanner that was used in {@link #addScanner(ButtonScanner)}
* @throws IOException
*/
public void removeScanner(ButtonScanner buttonScanner) throws IOException {
if (buttonScanner == null) {
throw new IllegalArgumentException("buttonScanner is null");
}
if (scanners.remove(buttonScanner.scanId) == null) {
throw new IllegalArgumentException("Button scanner was never added");
}
CmdRemoveScanner pkt = new CmdRemoveScanner();
pkt.scanId = buttonScanner.scanId;
sendPacket(pkt);
}
/**
* Add a scan wizard.
*
* The scan wizard will start directly once the scan wizard is added.
*
* @param scanWizard
* @throws IOException
*/
public void addScanWizard(ScanWizard scanWizard) throws IOException {
if (scanWizard == null) {
throw new IllegalArgumentException("scanWizard is null");
}
if (scanWizards.putIfAbsent(scanWizard.scanWizardId, scanWizard) != null) {
throw new IllegalArgumentException("Scan wizard already added");
}
CmdCreateScanWizard pkt = new CmdCreateScanWizard();
pkt.scanWizardId = scanWizard.scanWizardId;
sendPacket(pkt);
}
/**
* Cancel a scan wizard.
*
* This will cancel an ongoing scan wizard.
*
* If cancelled due to this request, the result of the scan wizard will be WizardCancelledByUser.
*
* @param scanWizard The same scan wizard that was used in {@link #addScanWizard(ScanWizard)}
* @throws IOException
*/
public void cancelScanWizard(ScanWizard scanWizard) throws IOException {
if (scanWizard == null) {
throw new IllegalArgumentException("scanWizard is null");
}
CmdCancelScanWizard pkt = new CmdCancelScanWizard();
pkt.scanWizardId = scanWizard.scanWizardId;
sendPacket(pkt);
}
/**
* Adds a connection channel to a specific Flic button.
*
* This will start listening for a specific Flic button's connection and button events.
* Make sure the Flic is either in public mode (by holding it down for 7 seconds) or already verified before calling this method.
*
* The {@link ButtonConnectionChannel.Callbacks#onCreateConnectionChannelResponse}
* method will be called after this command has been received by the server.
*
* You may have as many connection channels as you wish for a specific Flic Button.
*
* @param channel
* @throws IOException
*/
public void addConnectionChannel(ButtonConnectionChannel channel) throws IOException {
if (channel == null) {
throw new IllegalArgumentException("channel is null");
}
if (connectionChannels.putIfAbsent(channel.connId, channel) != null) {
throw new IllegalArgumentException("Connection channel already added");
}
synchronized (channel.lock) {
channel.client = this;
CmdCreateConnectionChannel pkt = new CmdCreateConnectionChannel();
pkt.connId = channel.connId;
pkt.bdaddr = channel.getBdaddr();
pkt.latencyMode = channel.getLatencyMode();
pkt.autoDisconnectTime = channel.getAutoDisconnectTime();
sendPacket(pkt);
}
}
/**
* Remove a connection channel.
*
* This will stop listening for new events for a specific connection channel that has previously been added.
* Note: The effect of this command will take place at the time the {@link ButtonConnectionChannel.Callbacks#onRemoved} event arrives.
*
* @param channel
* @throws IOException
*/
public void removeConnectionChannel(ButtonConnectionChannel channel) throws IOException {
if (channel == null) {
throw new IllegalArgumentException("channel is null");
}
CmdRemoveConnectionChannel pkt = new CmdRemoveConnectionChannel();
pkt.connId = channel.connId;
sendPacket(pkt);
}
/**
* Force disconnection or cancel pending connection of a specific Flic button.
*
* This removes all connection channels for all clients connected to the server for this specific Flic button.
*
* @param bdaddr
* @throws IOException
*/
public void forceDisconnect(Bdaddr bdaddr) throws IOException {
if (bdaddr == null) {
throw new IllegalArgumentException("bdaddr is null");
}
CmdForceDisconnect pkt = new CmdForceDisconnect();
pkt.bdaddr = bdaddr;
sendPacket(pkt);
}
/**
* Delete a button.
*
* @param bdaddr
* @throws IOException
*/
public void deleteButton(Bdaddr bdaddr) throws IOException {
if (bdaddr == null) {
throw new IllegalArgumentException("bdaddr is null");
}
CmdDeleteButton pkt = new CmdDeleteButton();
pkt.bdaddr = bdaddr;
sendPacket(pkt);
}
/**
* Add a battery status listener.
*
* @param listener
* @throws IOException
*/
public void addBatteryStatusListener(BatteryStatusListener listener) throws IOException {
if (listener == null) {
throw new IllegalArgumentException("listener is null");
}
if (batteryStatusListeners.putIfAbsent(listener.listenerId, listener) != null) {
throw new IllegalArgumentException("Battery status listener already added");
}
CmdCreateBatteryStatusListener pkt = new CmdCreateBatteryStatusListener();
pkt.listenerId = listener.listenerId;
pkt.bdaddr = listener.getBdaddr();
sendPacket(pkt);
}
/**
* Remove a battery status listener
*
* @param listener
* @throws IOException
*/
public void removeBatteryStatusListener(BatteryStatusListener listener) throws IOException {
if (listener == null) {
throw new IllegalArgumentException("buttonScanner is null");
}
if (batteryStatusListeners.remove(listener.listenerId) == null) {
throw new IllegalArgumentException("Battery status listener was never added");
}
CmdRemoveBatteryStatusListener pkt = new CmdRemoveBatteryStatusListener();
pkt.listenerId = listener.listenerId;
sendPacket(pkt);
}
void sendPacket(CommandPacket packet) throws IOException {
byte[] bytes = packet.construct();
synchronized (socketOutputStream) {
socketOutputStream.write(bytes);
}
}
/**
* Set a timer.
*
* This timer task will run after the specified timeoutMillis on the thread that handles the events.
*
* @param timeoutMillis
* @param timerTask
* @throws IOException
*/
public void setTimer(int timeoutMillis, TimerTask timerTask) throws IOException {
long pointInTime = System.nanoTime() + timeoutMillis * 1000000L;
while (timers.putIfAbsent(pointInTime, timerTask) != null) {
pointInTime++;
}
if (handleEventsThread != Thread.currentThread()) {
CmdPing pkt = new CmdPing();
pkt.pingId = 0;
sendPacket(pkt);
}
}
/**
* Run a task on the thread that handles the events.
*
* @param task
* @throws IOException
*/
public void runOnHandleEventsThread(TimerTask task) throws IOException {
if (handleEventsThread == Thread.currentThread()) {
task.run();
} else {
setTimer(0, task);
}
}
/**
* Start the main loop for this client.
*
* This method will not return until the socket has been closed.
* Once it has returned, any use of this FlicClient is illegal.
*
* @throws IOException
*/
public void handleEvents() throws IOException {
handleEventsThread = Thread.currentThread();
while (!Thread.currentThread().isInterrupted()) {
Map.Entry<Long, TimerTask> firstTimer = timers.firstEntry();
long timeout = 0;
if (firstTimer != null) {
timeout = firstTimer.getKey() - System.nanoTime();
if (timeout <= 0) {
timers.remove(firstTimer.getKey(), firstTimer.getValue());
firstTimer.getValue().run();
continue;
}
}
if (socket.isClosed()) {
break;
}
int len0;
socket.setSoTimeout((int)(timeout / 1000000));
try {
len0 = socketInputStream.read();
} catch (SocketTimeoutException e) {
continue;
}
int len1 = socketInputStream.read();
int len = len0 | (len1 << 8);
if ((len >> 16) == -1) {
break;
}
if (len == 0) {
continue;
}
byte[] pkt = new byte[len];
int pos = 0;
while (pos < len) {
int nbytes = socketInputStream.read(pkt, pos, len - pos);
if (nbytes == -1) {
break;
}
pos += nbytes;
}
if (len == 1) {
continue;
}
dispatchPacket(pkt);
}
socket.close();
}
private void dispatchPacket(byte[] packet) throws IOException {
int opcode = packet[0];
switch (opcode) {
case EventPacket.EVT_ADVERTISEMENT_PACKET_OPCODE: {
EvtAdvertisementPacket pkt = new EvtAdvertisementPacket();
pkt.parse(packet);
ButtonScanner scanner = scanners.get(pkt.scanId);
if (scanner != null) {
scanner.onAdvertisementPacket(pkt.addr, pkt.name, pkt.rssi, pkt.isPrivate, pkt.alreadyVerified, pkt.alreadyConnectedToThisDevice, pkt.alreadyConnectedToOtherDevice);
}
break;
}
case EventPacket.EVT_CREATE_CONNECTION_CHANNEL_RESPONSE_OPCODE: {
EvtCreateConnectionChannelResponse pkt = new EvtCreateConnectionChannelResponse();
pkt.parse(packet);
ButtonConnectionChannel channel = connectionChannels.get(pkt.connId);
if (channel != null) {
if (pkt.connectionChannelError != CreateConnectionChannelError.NoError) {
connectionChannels.remove(channel.connId);
}
channel.callbacks.onCreateConnectionChannelResponse(channel, pkt.connectionChannelError, pkt.connectionStatus);
}
break;
}
case EventPacket.EVT_CONNECTION_STATUS_CHANGED_OPCODE: {
EvtConnectionStatusChanged pkt = new EvtConnectionStatusChanged();
pkt.parse(packet);
ButtonConnectionChannel channel = connectionChannels.get(pkt.connId);
if (channel != null) {
channel.callbacks.onConnectionStatusChanged(channel, pkt.connectionStatus, pkt.disconnectReason);
}
break;
}
case EventPacket.EVT_CONNECTION_CHANNEL_REMOVED_OPCODE: {
EvtConnectionChannelRemoved pkt = new EvtConnectionChannelRemoved();
pkt.parse(packet);
ButtonConnectionChannel channel = connectionChannels.get(pkt.connId);
if (channel != null) {
connectionChannels.remove(channel.connId);
channel.callbacks.onRemoved(channel, pkt.removedReason);
}
break;
}
case EventPacket.EVT_BUTTON_UP_OR_DOWN_OPCODE:
case EventPacket.EVT_BUTTON_CLICK_OR_HOLD_OPCODE:
case EventPacket.EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OPCODE:
case EventPacket.EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OR_HOLD_OPCODE: {
EvtButtonEvent pkt = new EvtButtonEvent();
pkt.parse(packet);
ButtonConnectionChannel channel = connectionChannels.get(pkt.connId);
if (channel != null) {
if (opcode == EventPacket.EVT_BUTTON_UP_OR_DOWN_OPCODE) {
channel.callbacks.onButtonUpOrDown(channel, pkt.clickType, pkt.wasQueued, pkt.timeDiff);
} else if (opcode == EventPacket.EVT_BUTTON_CLICK_OR_HOLD_OPCODE) {
channel.callbacks.onButtonClickOrHold(channel, pkt.clickType, pkt.wasQueued, pkt.timeDiff);
} else if (opcode == EventPacket.EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OPCODE) {
channel.callbacks.onButtonSingleOrDoubleClick(channel, pkt.clickType, pkt.wasQueued, pkt.timeDiff);
} else if (opcode == EventPacket.EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OR_HOLD_OPCODE) {
channel.callbacks.onButtonSingleOrDoubleClickOrHold(channel, pkt.clickType, pkt.wasQueued, pkt.timeDiff);
}
}
break;
}
case EventPacket.EVT_NEW_VERIFIED_BUTTON_OPCODE: {
EvtNewVerifiedButton pkt = new EvtNewVerifiedButton();
pkt.parse(packet);
GeneralCallbacks gc = generalCallbacks;
if (gc != null) {
gc.onNewVerifiedButton(pkt.bdaddr);
}
break;
}
case EventPacket.EVT_GET_INFO_RESPONSE_OPCODE: {
EvtGetInfoResponse pkt = new EvtGetInfoResponse();
pkt.parse(packet);
getInfoResponseCallbackQueue.remove().onGetInfoResponse(pkt.bluetoothControllerState, pkt.myBdAddr, pkt.myBdAddrType, pkt.maxPendingConnections, pkt.maxConcurrentlyConnectedButtons, pkt.currentPendingConnections, pkt.currentlyNoSpaceForNewConnections, pkt.bdAddrOfVerifiedButtons);
break;
}
case EventPacket.EVT_NO_SPACE_FOR_NEW_CONNECTION_OPCODE: {
EvtNoSpaceForNewConnection pkt = new EvtNoSpaceForNewConnection();
pkt.parse(packet);
GeneralCallbacks gc = generalCallbacks;
if (gc != null) {
gc.onNoSpaceForNewConnection(pkt.maxConcurrentlyConnectedButtons);
}
break;
}
case EventPacket.EVT_GOT_SPACE_FOR_NEW_CONNECTION_OPCODE: {
EvtGotSpaceForNewConnection pkt = new EvtGotSpaceForNewConnection();
pkt.parse(packet);
GeneralCallbacks gc = generalCallbacks;
if (gc != null) {
gc.onGotSpaceForNewConnection(pkt.maxConcurrentlyConnectedButtons);
}
break;
}
case EventPacket.EVT_BLUETOOTH_CONTROLLER_STATE_CHANGE_OPCODE: {
EvtBluetoothControllerStateChange pkt = new EvtBluetoothControllerStateChange();
pkt.parse(packet);
GeneralCallbacks gc = generalCallbacks;
if (gc != null) {
gc.onBluetoothControllerStateChange(pkt.state);
}
break;
}
case EventPacket.EVT_GET_BUTTON_INFO_RESPONSE_OPCODE: {
EvtGetButtonInfoResponse pkt = new EvtGetButtonInfoResponse();
pkt.parse(packet);
getButtonInfoResponseCallbackQueue.remove().onGetButtonInfoResponse(pkt.bdaddr, pkt.uuid, pkt.color, pkt.serialNumber);
break;
}
case EventPacket.EVT_SCAN_WIZARD_FOUND_PRIVATE_BUTTON_OPCODE: {
EvtScanWizardFoundPrivateButton pkt = new EvtScanWizardFoundPrivateButton();
pkt.parse(packet);
ScanWizard wizard = scanWizards.get(pkt.scanWizardId);
if (wizard != null) {
wizard.onFoundPrivateButton();
}
break;
}
case EventPacket.EVT_SCAN_WIZARD_FOUND_PUBLIC_BUTTON_OPCODE: {
EvtScanWizardFoundPublicButton pkt = new EvtScanWizardFoundPublicButton();
pkt.parse(packet);
ScanWizard wizard = scanWizards.get(pkt.scanWizardId);
if (wizard != null) {
wizard.bdaddr = pkt.addr;
wizard.name = pkt.name;
wizard.onFoundPublicButton(wizard.bdaddr, wizard.name);
}
break;
}
case EventPacket.EVT_SCAN_WIZARD_BUTTON_CONNECTED_OPCODE: {
EvtScanWizardButtonConnected pkt = new EvtScanWizardButtonConnected();
pkt.parse(packet);
ScanWizard wizard = scanWizards.get(pkt.scanWizardId);
if (wizard != null) {
wizard.onButtonConnected(wizard.bdaddr, wizard.name);
}
break;
}
case EventPacket.EVT_SCAN_WIZARD_COMPLETED_OPCODE: {
EvtScanWizardCompleted pkt = new EvtScanWizardCompleted();
pkt.parse(packet);
ScanWizard wizard = scanWizards.get(pkt.scanWizardId);
scanWizards.remove(pkt.scanWizardId);
if (wizard != null) {
Bdaddr bdaddr = wizard.bdaddr;
String name = wizard.name;
wizard.bdaddr = null;
wizard.name = null;
wizard.onCompleted(pkt.result, bdaddr, name);
}
break;
}
case EventPacket.EVT_BUTTON_DELETED_OPCODE: {
EvtButtonDeleted pkt = new EvtButtonDeleted();
pkt.parse(packet);
GeneralCallbacks gc = generalCallbacks;
if (gc != null) {
gc.onButtonDeleted(pkt.bdaddr, pkt.deletedByThisClient);
}
break;
}
case EventPacket.EVT_BATTERY_STATUS_OPCODE: {
EvtBatteryStatus pkt = new EvtBatteryStatus();
pkt.parse(packet);
BatteryStatusListener listener = batteryStatusListeners.get(pkt.listenerId);
if (listener != null) {
listener.callbacks.onBatteryStatus(listener.getBdaddr(), pkt.batteryPercentage, pkt.timestamp);
}
break;
}
}
}
}

View File

@@ -0,0 +1,28 @@
package io.flic.fliclib.javaclient;
import io.flic.fliclib.javaclient.enums.BluetoothControllerState;
import java.io.IOException;
/**
* GeneralCallbacks.
*
* See the protocol specification for further details.
*/
public class GeneralCallbacks {
public void onNewVerifiedButton(Bdaddr bdaddr) throws IOException {
}
public void onNoSpaceForNewConnection(int maxConcurrentlyConnectedButtons) throws IOException {
}
public void onGotSpaceForNewConnection(int maxConcurrentlyConnectedButtons) throws IOException {
}
public void onBluetoothControllerStateChange(BluetoothControllerState state) throws IOException {
}
public void onButtonDeleted(Bdaddr bdaddr, boolean deletedByThisClient) throws IOException {
}
}

View File

@@ -0,0 +1,18 @@
package io.flic.fliclib.javaclient;
/**
* GetButtonInfoResponseCallback.
*
* Used in {@link FlicClient#getButtonInfo(Bdaddr, GetButtonInfoResponseCallback)}.
*/
public abstract class GetButtonInfoResponseCallback {
/**
* Called upon response.
*
* @param bdaddr Bluetooth address
* @param uuid Uuid of button, might be null if unknown
* @param color Color of button, might be null if unknown
* @param serialNumber Serial number of the button, will be null if the button is not found
*/
public abstract void onGetButtonInfoResponse(Bdaddr bdaddr, String uuid, String color, String serialNumber);
}

View File

@@ -0,0 +1,19 @@
package io.flic.fliclib.javaclient;
import io.flic.fliclib.javaclient.enums.BdAddrType;
import io.flic.fliclib.javaclient.enums.BluetoothControllerState;
import java.io.IOException;
/**
* GetInfoResponseCallback.
*
* Used in {@link FlicClient#getInfo(GetInfoResponseCallback)}.
*/
public abstract class GetInfoResponseCallback {
public abstract void onGetInfoResponse(BluetoothControllerState bluetoothControllerState, Bdaddr myBdAddr,
BdAddrType myBdAddrType, int maxPendingConnections,
int maxConcurrentlyConnectedButtons, int currentPendingConnections,
boolean currentlyNoSpaceForNewConnection,
Bdaddr[] verifiedButtons) throws IOException;
}

View File

@@ -0,0 +1,455 @@
package io.flic.fliclib.javaclient;
import java.io.*;
import java.nio.charset.StandardCharsets;
import io.flic.fliclib.javaclient.enums.*;
/**
* Flic Protocol Packets
*/
abstract class CommandPacket {
protected int opcode;
public final byte[] construct() {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
try {
write(stream);
} catch (IOException e) {
}
byte[] res = new byte[3 + stream.size()];
res[0] = (byte)(1 + stream.size());
res[1] = (byte)((1 + stream.size()) >> 8);
res[2] = (byte)opcode;
System.arraycopy(stream.toByteArray(), 0, res, 3, stream.size());
return res;
}
abstract protected void write(OutputStream stream) throws IOException;
}
class CmdGetInfo extends CommandPacket {
@Override
protected void write(OutputStream stream) {
opcode = 0;
}
}
class CmdCreateScanner extends CommandPacket {
public int scanId;
@Override
protected void write(OutputStream stream) throws IOException {
opcode = 1;
StreamUtils.writeInt32(stream, scanId);
}
}
class CmdRemoveScanner extends CommandPacket {
public int scanId;
@Override
protected void write(OutputStream stream) throws IOException {
opcode = 2;
StreamUtils.writeInt32(stream, scanId);
}
}
class CmdCreateConnectionChannel extends CommandPacket {
public int connId;
public Bdaddr bdaddr;
public LatencyMode latencyMode;
public short autoDisconnectTime;
@Override
protected void write(OutputStream stream) throws IOException {
opcode = 3;
StreamUtils.writeInt32(stream, connId);
StreamUtils.writeBdaddr(stream, bdaddr);
StreamUtils.writeEnum(stream, latencyMode);
StreamUtils.writeInt16(stream, autoDisconnectTime);
}
}
class CmdRemoveConnectionChannel extends CommandPacket {
public int connId;
@Override
protected void write(OutputStream stream) throws IOException {
opcode = 4;
StreamUtils.writeInt32(stream, connId);
}
}
class CmdForceDisconnect extends CommandPacket {
public Bdaddr bdaddr;
@Override
protected void write(OutputStream stream) throws IOException {
opcode = 5;
StreamUtils.writeBdaddr(stream, bdaddr);
}
}
class CmdChangeModeParameters extends CommandPacket {
public int connId;
public LatencyMode latencyMode;
public short autoDisconnectTime;
@Override
protected void write(OutputStream stream) throws IOException {
opcode = 6;
StreamUtils.writeInt32(stream, connId);
StreamUtils.writeEnum(stream, latencyMode);
StreamUtils.writeInt16(stream, autoDisconnectTime);
}
}
class CmdPing extends CommandPacket {
public int pingId;
@Override
protected void write(OutputStream stream) throws IOException {
opcode = 7;
StreamUtils.writeInt32(stream, pingId);
}
}
class CmdGetButtonInfo extends CommandPacket {
public Bdaddr bdaddr;
@Override
protected void write(OutputStream stream) throws IOException {
opcode = 8;
StreamUtils.writeBdaddr(stream, bdaddr);
}
}
class CmdCreateScanWizard extends CommandPacket {
public int scanWizardId;
@Override
protected void write(OutputStream stream) throws IOException {
opcode = 9;
StreamUtils.writeInt32(stream, scanWizardId);
}
}
class CmdCancelScanWizard extends CommandPacket {
public int scanWizardId;
@Override
protected void write(OutputStream stream) throws IOException {
opcode = 10;
StreamUtils.writeInt32(stream, scanWizardId);
}
}
class CmdDeleteButton extends CommandPacket {
public Bdaddr bdaddr;
@Override
protected void write(OutputStream stream) throws IOException {
opcode = 11;
StreamUtils.writeBdaddr(stream, bdaddr);
}
}
class CmdCreateBatteryStatusListener extends CommandPacket {
public int listenerId;
public Bdaddr bdaddr;
@Override
protected void write(OutputStream stream) throws IOException {
opcode = 12;
StreamUtils.writeInt32(stream, listenerId);
StreamUtils.writeBdaddr(stream, bdaddr);
}
}
class CmdRemoveBatteryStatusListener extends CommandPacket {
public int listenerId;
@Override
protected void write(OutputStream stream) throws IOException {
opcode = 13;
StreamUtils.writeInt32(stream, listenerId);
}
}
abstract class EventPacket {
public static final int EVT_ADVERTISEMENT_PACKET_OPCODE = 0;
public static final int EVT_CREATE_CONNECTION_CHANNEL_RESPONSE_OPCODE = 1;
public static final int EVT_CONNECTION_STATUS_CHANGED_OPCODE = 2;
public static final int EVT_CONNECTION_CHANNEL_REMOVED_OPCODE = 3;
public static final int EVT_BUTTON_UP_OR_DOWN_OPCODE = 4;
public static final int EVT_BUTTON_CLICK_OR_HOLD_OPCODE = 5;
public static final int EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OPCODE = 6;
public static final int EVT_BUTTON_SINGLE_OR_DOUBLE_CLICK_OR_HOLD_OPCODE = 7;
public static final int EVT_NEW_VERIFIED_BUTTON_OPCODE = 8;
public static final int EVT_GET_INFO_RESPONSE_OPCODE = 9;
public static final int EVT_NO_SPACE_FOR_NEW_CONNECTION_OPCODE = 10;
public static final int EVT_GOT_SPACE_FOR_NEW_CONNECTION_OPCODE = 11;
public static final int EVT_BLUETOOTH_CONTROLLER_STATE_CHANGE_OPCODE = 12;
public static final int EVT_PING_RESPONSE_OPCODE = 13;
public static final int EVT_GET_BUTTON_INFO_RESPONSE_OPCODE = 14;
public static final int EVT_SCAN_WIZARD_FOUND_PRIVATE_BUTTON_OPCODE = 15;
public static final int EVT_SCAN_WIZARD_FOUND_PUBLIC_BUTTON_OPCODE = 16;
public static final int EVT_SCAN_WIZARD_BUTTON_CONNECTED_OPCODE = 17;
public static final int EVT_SCAN_WIZARD_COMPLETED_OPCODE = 18;
public static final int EVT_BUTTON_DELETED_OPCODE = 19;
public static final int EVT_BATTERY_STATUS_OPCODE = 20;
public void parse(byte[] arr) {
InputStream stream = new ByteArrayInputStream(arr);
try {
stream.skip(1);
parseInternal(stream);
} catch(IOException e) {
}
}
abstract protected void parseInternal(InputStream stream) throws IOException;
}
class EvtAdvertisementPacket extends EventPacket {
public int scanId;
public Bdaddr addr;
public String name;
public int rssi;
public boolean isPrivate;
public boolean alreadyVerified;
public boolean alreadyConnectedToThisDevice;
public boolean alreadyConnectedToOtherDevice;
@Override
protected void parseInternal(InputStream stream) throws IOException {
scanId = StreamUtils.getInt32(stream);
addr = StreamUtils.getBdaddr(stream);
name = StreamUtils.getString(stream, 16);
rssi = StreamUtils.getInt8(stream);
isPrivate = StreamUtils.getBoolean(stream);
alreadyVerified = StreamUtils.getBoolean(stream);
alreadyConnectedToThisDevice = StreamUtils.getBoolean(stream);
alreadyConnectedToOtherDevice = StreamUtils.getBoolean(stream);
}
}
class EvtCreateConnectionChannelResponse extends EventPacket {
public int connId;
public CreateConnectionChannelError connectionChannelError;
public ConnectionStatus connectionStatus;
@Override
protected void parseInternal(InputStream stream) throws IOException {
connId = StreamUtils.getInt32(stream);
connectionChannelError = CreateConnectionChannelError.values()[StreamUtils.getUInt8(stream)];
connectionStatus = ConnectionStatus.values()[StreamUtils.getUInt8(stream)];
}
}
class EvtConnectionStatusChanged extends EventPacket {
public int connId;
public ConnectionStatus connectionStatus;
public DisconnectReason disconnectReason;
@Override
protected void parseInternal(InputStream stream) throws IOException {
connId = StreamUtils.getInt32(stream);
connectionStatus = ConnectionStatus.values()[StreamUtils.getUInt8(stream)];
disconnectReason = DisconnectReason.values()[StreamUtils.getUInt8(stream)];
}
}
class EvtConnectionChannelRemoved extends EventPacket {
public int connId;
public RemovedReason removedReason;
@Override
protected void parseInternal(InputStream stream) throws IOException {
connId = StreamUtils.getInt32(stream);
removedReason = RemovedReason.values()[StreamUtils.getUInt8(stream)];
}
}
class EvtButtonEvent extends EventPacket {
public int connId;
public ClickType clickType;
public boolean wasQueued;
public int timeDiff;
@Override
protected void parseInternal(InputStream stream) throws IOException {
connId = StreamUtils.getInt32(stream);
clickType = ClickType.values()[StreamUtils.getUInt8(stream)];
wasQueued = StreamUtils.getBoolean(stream);
timeDiff = StreamUtils.getInt32(stream);
}
}
class EvtNewVerifiedButton extends EventPacket {
public Bdaddr bdaddr;
@Override
protected void parseInternal(InputStream stream) throws IOException {
bdaddr = StreamUtils.getBdaddr(stream);
}
}
class EvtGetInfoResponse extends EventPacket {
public BluetoothControllerState bluetoothControllerState;
public Bdaddr myBdAddr;
public BdAddrType myBdAddrType;
public int maxPendingConnections;
public int maxConcurrentlyConnectedButtons;
public int currentPendingConnections;
public boolean currentlyNoSpaceForNewConnections;
public Bdaddr[] bdAddrOfVerifiedButtons;
@Override
protected void parseInternal(InputStream stream) throws IOException {
bluetoothControllerState = BluetoothControllerState.values()[StreamUtils.getUInt8(stream)];
myBdAddr = StreamUtils.getBdaddr(stream);
myBdAddrType = BdAddrType.values()[StreamUtils.getUInt8(stream)];
maxPendingConnections = StreamUtils.getUInt8(stream);
maxConcurrentlyConnectedButtons = StreamUtils.getInt16(stream);
currentPendingConnections = StreamUtils.getUInt8(stream);
currentlyNoSpaceForNewConnections = StreamUtils.getBoolean(stream);
int nbVerifiedButtons = StreamUtils.getUInt16(stream);
bdAddrOfVerifiedButtons = new Bdaddr[nbVerifiedButtons];
for (int i = 0; i < nbVerifiedButtons; i++) {
bdAddrOfVerifiedButtons[i] = StreamUtils.getBdaddr(stream);
}
}
}
class EvtNoSpaceForNewConnection extends EventPacket {
public int maxConcurrentlyConnectedButtons;
@Override
protected void parseInternal(InputStream stream) throws IOException {
maxConcurrentlyConnectedButtons = StreamUtils.getUInt8(stream);
}
}
class EvtGotSpaceForNewConnection extends EventPacket {
public int maxConcurrentlyConnectedButtons;
@Override
protected void parseInternal(InputStream stream) throws IOException {
maxConcurrentlyConnectedButtons = StreamUtils.getUInt8(stream);
}
}
class EvtBluetoothControllerStateChange extends EventPacket {
public BluetoothControllerState state;
@Override
protected void parseInternal(InputStream stream) throws IOException {
state = BluetoothControllerState.values()[StreamUtils.getUInt8(stream)];
}
}
class EvtGetButtonInfoResponse extends EventPacket {
public Bdaddr bdaddr;
public String uuid;
public String color;
public String serialNumber;
@Override
protected void parseInternal(InputStream stream) throws IOException {
bdaddr = StreamUtils.getBdaddr(stream);
byte[] uuidBytes = StreamUtils.getByteArr(stream, 16);
StringBuilder sb = new StringBuilder(32);
for (int i = 0; i < 16; i++) {
sb.append(String.format("%02x", uuidBytes[i]));
}
uuid = sb.toString();
if (uuid.equals("00000000000000000000000000000000")) {
uuid = null;
}
color = StreamUtils.getString(stream, 16);
if (color.isEmpty()) {
color = null;
}
serialNumber = StreamUtils.getString(stream, 16);
if (serialNumber.isEmpty()) {
serialNumber = null;
}
}
}
class EvtScanWizardFoundPrivateButton extends EventPacket {
public int scanWizardId;
@Override
protected void parseInternal(InputStream stream) throws IOException {
scanWizardId = StreamUtils.getInt32(stream);
}
}
class EvtScanWizardFoundPublicButton extends EventPacket {
public int scanWizardId;
public Bdaddr addr;
public String name;
@Override
protected void parseInternal(InputStream stream) throws IOException {
scanWizardId = StreamUtils.getInt32(stream);
addr = StreamUtils.getBdaddr(stream);
int nameLen = StreamUtils.getUInt8(stream);
byte[] bytes = new byte[nameLen];
for (int i = 0; i < nameLen; i++) {
bytes[i] = (byte)stream.read();
}
for (int i = nameLen; i < 16; i++) {
stream.skip(1);
}
name = new String(bytes, StandardCharsets.UTF_8);
}
}
class EvtScanWizardButtonConnected extends EventPacket {
public int scanWizardId;
@Override
protected void parseInternal(InputStream stream) throws IOException {
scanWizardId = StreamUtils.getInt32(stream);
}
}
class EvtScanWizardCompleted extends EventPacket {
public int scanWizardId;
public ScanWizardResult result;
@Override
protected void parseInternal(InputStream stream) throws IOException {
scanWizardId = StreamUtils.getInt32(stream);
result = ScanWizardResult.values()[StreamUtils.getUInt8(stream)];
}
}
class EvtButtonDeleted extends EventPacket {
public Bdaddr bdaddr;
public boolean deletedByThisClient;
@Override
protected void parseInternal(InputStream stream) throws IOException {
bdaddr = StreamUtils.getBdaddr(stream);
deletedByThisClient = StreamUtils.getBoolean(stream);
}
}
class EvtBatteryStatus extends EventPacket {
public int listenerId;
public int batteryPercentage;
public long timestamp;
@Override
protected void parseInternal(InputStream stream) throws IOException {
listenerId = StreamUtils.getInt32(stream);
batteryPercentage = StreamUtils.getInt8(stream);
timestamp = StreamUtils.getInt64(stream);
}
}

View File

@@ -0,0 +1,64 @@
package io.flic.fliclib.javaclient;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import io.flic.fliclib.javaclient.enums.ScanWizardResult;
/**
* Scan wizard class.
*
* This class will scan for a new button and pair it automatically.
* There are internal timeouts that make sure operations don't take too long time.
*
* Inherit this class and override the methods.
* Then add this scan wizard to a {@link FlicClient} using {@link FlicClient#addScanWizard(ScanWizard)} to start it.
* You can cancel by calling {@link FlicClient#cancelScanWizard(ScanWizard)}.
*/
public abstract class ScanWizard {
private static AtomicInteger nextId = new AtomicInteger();
int scanWizardId = nextId.getAndIncrement();
Bdaddr bdaddr;
String name;
/**
* This will be called once if a private button is found.
*
* Tell the user to hold down the button for 7 seconds in order to make it public.
*
*/
public abstract void onFoundPrivateButton() throws IOException;
/**
* This will be called once a public button is found.
*
* Now a connection attempt will be made to the device in order to pair and verify it.
*
* @param bdaddr Bluetooth Device Address
* @param name Advertising name
*/
public abstract void onFoundPublicButton(Bdaddr bdaddr, String name) throws IOException;
/**
* This will be called once the bluetooth connection has been established.
*
* Now a pair attempt will be made.
*
* @param bdaddr Bluetooth Device Address
* @param name Advertising name
*/
public abstract void onButtonConnected(Bdaddr bdaddr, String name) throws IOException;
/**
* Scan wizard completed.
*
* If the result is success, you can now create a connection channel to the button.
*
* The ScanWizard is now detached from the FlicClient and can now be recycled.
*
* @param result Result of the scan wizard
* @param bdaddr Bluetooth Device Address or null, depending on if {@link #onFoundPublicButton} has been called or not
* @param name Advertising name or null, depending on if {@link #onFoundPublicButton} has been called or not
*/
public abstract void onCompleted(ScanWizardResult result, Bdaddr bdaddr, String name) throws IOException;
}

View File

@@ -0,0 +1,81 @@
package io.flic.fliclib.javaclient;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
class StreamUtils {
public static boolean getBoolean(InputStream stream) throws IOException {
return stream.read() != 0;
}
public static int getUInt8(InputStream stream) throws IOException {
return stream.read();
}
public static int getInt8(InputStream stream) throws IOException {
return (byte)stream.read();
}
public static int getUInt16(InputStream stream) throws IOException {
return stream.read() | (stream.read() << 8);
}
public static int getInt16(InputStream stream) throws IOException {
return (short)getUInt16(stream);
}
public static int getInt32(InputStream stream) throws IOException {
return stream.read() | (stream.read() << 8) | (stream.read() << 16) | (stream.read() << 24);
}
public static long getInt64(InputStream stream) throws IOException {
return (getInt32(stream) & 0xffffffffL) | ((long)getInt32(stream) << 32);
}
public static Bdaddr getBdaddr(InputStream stream) throws IOException {
return new Bdaddr(stream);
}
public static byte[] getByteArr(InputStream stream, int len) throws IOException {
byte[] arr = new byte[len];
for (int i = 0; i < len; i++) {
arr[i] = (byte)stream.read();
}
return arr;
}
public static String getString(InputStream stream, int maxlen) throws IOException {
int len = getInt8(stream);
byte[] arr = new byte[len];
for (int i = 0; i < len; i++) {
arr[i] = (byte)stream.read();
}
for (int i = len; i < maxlen; i++) {
stream.skip(1);
}
return new String(arr, StandardCharsets.UTF_8);
}
public static void writeEnum(OutputStream stream, Enum<?> enumValue) throws IOException {
stream.write(enumValue.ordinal());
}
public static void writeInt8(OutputStream stream, int v) throws IOException {
stream.write(v);
}
public static void writeInt16(OutputStream stream, int v) throws IOException {
stream.write(v & 0xff);
stream.write(v >> 8);
}
public static void writeInt32(OutputStream stream, int v) throws IOException {
writeInt16(stream, v);
writeInt16(stream, v >> 16);
}
public static void writeBdaddr(OutputStream stream, Bdaddr addr) throws IOException {
stream.write(addr.getBytes());
}
}

View File

@@ -0,0 +1,14 @@
package io.flic.fliclib.javaclient;
import java.io.IOException;
/**
* TimerTask.
*
* Use this interface instead of {@link Runnable} to avoid having to deal with IOExceptions.
* Invocations of the run method on this interface from the {@link FlicClient} will propagate IOExceptions to the caller of {@link FlicClient#handleEvents()}.
*
*/
public interface TimerTask {
void run() throws IOException;
}

View File

@@ -0,0 +1,9 @@
package io.flic.fliclib.javaclient.enums;
/**
* Created by Emil on 2016-05-03.
*/
public enum BdAddrType {
PublicBdAddrType,
RandomBdAddrType
}

View File

@@ -0,0 +1,10 @@
package io.flic.fliclib.javaclient.enums;
/**
* Created by Emil on 2016-05-03.
*/
public enum BluetoothControllerState {
Detached,
Resetting,
Attached
}

View File

@@ -0,0 +1,13 @@
package io.flic.fliclib.javaclient.enums;
/**
* Created by Emil on 2016-05-03.
*/
public enum ClickType {
ButtonDown,
ButtonUp,
ButtonClick,
ButtonSingleClick,
ButtonDoubleClick,
ButtonHold
}

View File

@@ -0,0 +1,10 @@
package io.flic.fliclib.javaclient.enums;
/**
* Created by Emil on 2016-05-03.
*/
public enum ConnectionStatus {
Disconnected,
Connected,
Ready
}

View File

@@ -0,0 +1,9 @@
package io.flic.fliclib.javaclient.enums;
/**
* Created by Emil on 2016-05-03.
*/
public enum CreateConnectionChannelError {
NoError,
MaxPendingConnectionsReached
}

View File

@@ -0,0 +1,11 @@
package io.flic.fliclib.javaclient.enums;
/**
* Created by Emil on 2016-05-03.
*/
public enum DisconnectReason {
Unspecified,
ConnectionEstablishmentFailed,
TimedOut,
BondingKeysMismatch
}

View File

@@ -0,0 +1,10 @@
package io.flic.fliclib.javaclient.enums;
/**
* Created by Emil on 2016-05-03.
*/
public enum LatencyMode {
NormalLatency,
LowLatency,
HighLatency
}

View File

@@ -0,0 +1,22 @@
package io.flic.fliclib.javaclient.enums;
/**
* Created by Emil on 2016-05-03.
*/
public enum RemovedReason {
RemovedByThisClient,
ForceDisconnectedByThisClient,
ForceDisconnectedByOtherClient,
ButtonIsPrivate,
VerifyTimeout,
InternetBackendError,
InvalidData,
CouldntLoadDevice,
DeletedByThisClient,
DeletedByOtherClient,
ButtonBelongsToOtherPartner,
DeletedFromButton
}

View File

@@ -0,0 +1,13 @@
package io.flic.fliclib.javaclient.enums;
public enum ScanWizardResult {
WizardSuccess,
WizardCancelledByUser,
WizardFailedTimeout,
WizardButtonIsPrivate,
WizardBluetoothUnavailable,
WizardInternetBackendError,
WizardInvalidData,
WizardButtonBelongsToOtherPartner,
WizardButtonAlreadyConnectedToOtherDevice
}