From 7ebf206688af2d3e193d8c2e6b409c40edc00bdd Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Mon, 9 Jul 2018 13:40:54 +0200 Subject: [PATCH 1/9] add ANT --- .idea/gradle.xml | 1 + .idea/misc.xml | 2 +- .idea/modules.xml | 1 + Application/Application.iml | 13 +- Application/build.gradle | 2 + Application/src/main/AndroidManifest.xml | 1 + .../dsi/ant/channel/PredefinedNetwork.java | 40 +++ .../iconsole/BluetoothChatFragment.java | 69 ++++ .../org/surfsite/iconsole/ChannelService.java | 262 +++++++++++++++ .../org/surfsite/iconsole/MainActivity.java | 12 +- .../iconsole/PowerChannelController.java | 292 +++++++++++++++++ .../iconsole/SpeedChannelController.java | 306 ++++++++++++++++++ android_antlib_4-14/android_antlib_4-14.iml | 25 ++ android_antlib_4-14/build.gradle | 2 + settings.gradle | 5 +- 15 files changed, 1026 insertions(+), 7 deletions(-) create mode 100644 Application/src/main/java/com/dsi/ant/channel/PredefinedNetwork.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/ChannelService.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/PowerChannelController.java create mode 100644 Application/src/main/java/org/surfsite/iconsole/SpeedChannelController.java create mode 100644 android_antlib_4-14/android_antlib_4-14.iml create mode 100644 android_antlib_4-14/build.gradle diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 773d366..1c53797 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -9,6 +9,7 @@ - + diff --git a/.idea/modules.xml b/.idea/modules.xml index c8421df..14a85c1 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -3,6 +3,7 @@ + diff --git a/Application/Application.iml b/Application/Application.iml index bb90e68..153210c 100644 --- a/Application/Application.iml +++ b/Application/Application.iml @@ -90,17 +90,27 @@ + + + + + + + - + + + + @@ -130,5 +140,6 @@ + \ No newline at end of file diff --git a/Application/build.gradle b/Application/build.gradle index 232ebea..8f87031 100644 --- a/Application/build.gradle +++ b/Application/build.gradle @@ -29,8 +29,10 @@ dependencies { implementation "com.android.support:appcompat-v7:27.1.1" implementation 'com.android.support:support-v13:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.2' + implementation project(':android_antlib_4-14') } + // The sample build uses multiple directories to // keep boilerplate and common code separate from // the main sample code. diff --git a/Application/src/main/AndroidManifest.xml b/Application/src/main/AndroidManifest.xml index 01cfa2b..2b01c64 100644 --- a/Application/src/main/AndroidManifest.xml +++ b/Application/src/main/AndroidManifest.xml @@ -43,6 +43,7 @@ + 0; + // Initial check for if legacy interface is in use. If the + // legacy interface is in use, applications can free the ANT + // radio by attempting to acquire a channel. + boolean legacyInterfaceInUse = mAntChannelProvider.isLegacyInterfaceInUse(); + + // If there are channels OR legacy interface in use, allow adding channels + if (mChannelAvailable || legacyInterfaceInUse) { + mAllowAddChannel = true; + } else { + // If no channels available AND legacy interface is not in use, disallow adding channels + mAllowAddChannel = false; + } + + + } catch (RemoteException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + die("Binder Died"); + + mAntChannelProvider = null; + mAntRadioService = null; + + mAllowAddChannel = false; + } + + }; + + /** + * The interface used to communicate with the ChannelService + */ + public class ChannelServiceComm extends Binder { + + void setSpeed(double speed) { + if (null != speedChannelController) { + speedChannelController.speed = speed; + } + } + + void setPower(int power) { + if (null != powerChannelController) { + powerChannelController.power = power; + } + } + + void setCadence(int cadence) { + if (null != powerChannelController) { + powerChannelController.cadence = cadence; + } + } + + /** + * Closes all channels currently added. + */ + void clearAllChannels() { + closeAllChannels(); + } + } + + public void openAllChannels() throws ChannelNotAvailableException { + powerChannelController = new PowerChannelController(acquireChannel()); + speedChannelController = new SpeedChannelController(acquireChannel()); + } + + private void closeAllChannels() { + powerChannelController.close(); + speedChannelController.close(); + powerChannelController = null; + speedChannelController = null; + } + + AntChannel acquireChannel() throws ChannelNotAvailableException { + AntChannel mAntChannel = null; + if (null != mAntChannelProvider) { + try { + /* + * If applications require a channel with specific capabilities + * (event buffering, background scanning etc.), a Capabilities + * object should be created and then the specific capabilities + * required set to true. Applications can specify both required + * and desired Capabilities with both being passed in + * acquireChannel(context, PredefinedNetwork, + * requiredCapabilities, desiredCapabilities). + */ + mAntChannel = mAntChannelProvider.acquireChannel(this, PredefinedNetwork.ANT_PLUS_1); + /* + NetworkKey mNK = new NetworkKey(new byte[] { (byte)0xb9, (byte)0xa5, (byte)0x21, (byte)0xfb, + (byte)0xbd, (byte)0x72, (byte)0xc3, (byte)0x45 }); + Log.v(TAG, mNK.toString()); + mAntChannel = mAntChannelProvider.acquireChannelOnPrivateNetwork(this, mNK); + */ + } catch (RemoteException e) { + die("ACP Remote Ex"); + } + } + return mAntChannel; + } + + @Override + public IBinder onBind(Intent arg0) { + return new ChannelServiceComm(); + } + + /** + * Receives AntChannelProvider state changes being sent from ANT Radio Service + */ + private final BroadcastReceiver mChannelProviderStateChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (AntChannelProvider.ACTION_CHANNEL_PROVIDER_STATE_CHANGED.equals(intent.getAction())) { + boolean update = false; + // Retrieving the data contained in the intent + int numChannels = intent.getIntExtra(AntChannelProvider.NUM_CHANNELS_AVAILABLE, 0); + boolean legacyInterfaceInUse = intent.getBooleanExtra(AntChannelProvider.LEGACY_INTERFACE_IN_USE, false); + + if (mAllowAddChannel) { + // Was a acquire channel allowed + // If no channels available AND legacy interface is not in use, disallow acquiring of channels + if (0 == numChannels && !legacyInterfaceInUse) { + mAllowAddChannel = false; + update = true; + closeAllChannels(); + } + } else { + // Acquire channels not allowed + // If there are channels OR legacy interface in use, allow acquiring of channels + if (numChannels > 0 || legacyInterfaceInUse) { + mAllowAddChannel = true; + update = true; + try { + openAllChannels(); + } catch (ChannelNotAvailableException exception) { + Log.e(TAG, "Channel not available!!"); + } + } + } + } + } + }; + + private void doBindAntRadioService() { + if (BuildConfig.DEBUG) Log.v(TAG, "doBindAntRadioService"); + + // Start listing for channel available intents + registerReceiver(mChannelProviderStateChangedReceiver, new IntentFilter(AntChannelProvider.ACTION_CHANNEL_PROVIDER_STATE_CHANGED)); + + // Creating the intent and calling context.bindService() is handled by + // the static bindService() method in AntService + mAntRadioServiceBound = AntService.bindService(this, mAntRadioServiceConnection); + } + + private void doUnbindAntRadioService() { + if (BuildConfig.DEBUG) Log.v(TAG, "doUnbindAntRadioService"); + + // Stop listing for channel available intents + try { + unregisterReceiver(mChannelProviderStateChangedReceiver); + } catch (IllegalArgumentException exception) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Attempting to unregister a never registered Channel Provider State Changed receiver."); + } + + if (mAntRadioServiceBound) { + try { + unbindService(mAntRadioServiceConnection); + } catch (IllegalArgumentException e) { + // Not bound, that's what we want anyway + } + + mAntRadioServiceBound = false; + } + } + + @Override + public void onCreate() { + super.onCreate(); + + mAntRadioServiceBound = false; + + doBindAntRadioService(); + + } + + @Override + public void onDestroy() { + closeAllChannels(); + + doUnbindAntRadioService(); + mAntChannelProvider = null; + + super.onDestroy(); + } + + static void die(String error) { + Log.e(TAG, "DIE: " + error); + } + +} diff --git a/Application/src/main/java/org/surfsite/iconsole/MainActivity.java b/Application/src/main/java/org/surfsite/iconsole/MainActivity.java index 6e26949..b917357 100644 --- a/Application/src/main/java/org/surfsite/iconsole/MainActivity.java +++ b/Application/src/main/java/org/surfsite/iconsole/MainActivity.java @@ -30,8 +30,10 @@ import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.WindowManager; +import android.widget.Button; import android.widget.ViewAnimator; +import org.surfsite.iconsole.ChannelService.ChannelServiceComm; /** @@ -70,8 +72,16 @@ public class MainActivity extends FragmentActivity { @Override protected void onDestroy() { - stopService(new Intent(this, BluetoothChatService.class)); + + if(isFinishing()) + { + stopService(new Intent(this, BluetoothChatService.class)); + stopService(new Intent(this, ChannelService.class)); + } super.onDestroy(); } + + + } diff --git a/Application/src/main/java/org/surfsite/iconsole/PowerChannelController.java b/Application/src/main/java/org/surfsite/iconsole/PowerChannelController.java new file mode 100644 index 0000000..d678426 --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/PowerChannelController.java @@ -0,0 +1,292 @@ +/* + * Copyright 2012 Dynastream Innovations Inc. + * + * 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 org.surfsite.iconsole; + +import android.os.RemoteException; +import android.util.Log; + +import com.dsi.ant.channel.AntChannel; +import com.dsi.ant.channel.AntCommandFailedException; +import com.dsi.ant.channel.IAntChannelEventHandler; +import com.dsi.ant.message.ChannelId; +import com.dsi.ant.message.ChannelType; +import com.dsi.ant.message.EventCode; +import com.dsi.ant.message.fromant.AcknowledgedDataMessage; +import com.dsi.ant.message.fromant.ChannelEventMessage; +import com.dsi.ant.message.fromant.MessageFromAntType; +import com.dsi.ant.message.ipc.AntMessageParcel; + +import java.util.Random; + +public class PowerChannelController { + public static final int POWER_SENSOR_ID = 0x9e3d4b66; + // The device type and transmission type to be part of the channel ID message + private static final int CHANNEL_POWER_DEVICE_TYPE = 0x0B; + private static final int CHANNEL_POWER_TRANSMISSION_TYPE = 5; + // The period and frequency values the channel will be configured to + private static final int CHANNEL_POWER_PERIOD = 8182; // 1 Hz + private static final int CHANNEL_POWER_FREQUENCY = 57; + private static final String TAG = PowerChannelController.class.getSimpleName(); + private static Random randGen = new Random(); + int power = 0; + int cadence = 0; + private AntChannel mAntChannel; + private ChannelEventCallback mChannelEventCallback = new ChannelEventCallback(); + private boolean mIsOpen; + + public PowerChannelController(AntChannel antChannel) { + mAntChannel = antChannel; + openChannel(); + } + + boolean openChannel() { + if (null != mAntChannel) { + if (mIsOpen) { + Log.w(TAG, "Channel was already open"); + } else { + // Channel ID message contains device number, type and transmission type. In + // order for master (TX) channels and slave (RX) channels to connect, they + // must have the same channel ID, or wildcard (0) is used. + ChannelId channelId = new ChannelId(POWER_SENSOR_ID & 0xFFFF, + CHANNEL_POWER_DEVICE_TYPE, CHANNEL_POWER_TRANSMISSION_TYPE); + + try { + // Setting the channel event handler so that we can receive messages from ANT + mAntChannel.setChannelEventHandler(mChannelEventCallback); + + // Performs channel assignment by assigning the type to the channel. Additional + // features (such as, background scanning and frequency agility) can be enabled + // by passing an ExtendedAssignment object to assign(ChannelType, ExtendedAssignment). + mAntChannel.assign(ChannelType.BIDIRECTIONAL_MASTER); + + /* + * Configures the channel ID, messaging period and rf frequency after assigning, + * then opening the channel. + * + * For any additional ANT features such as proximity search or background scanning, refer to + * the ANT Protocol Doc found at: + * http://www.thisisant.com/resources/ant-message-protocol-and-usage/ + */ + mAntChannel.setChannelId(channelId); + mAntChannel.setPeriod(CHANNEL_POWER_PERIOD); + mAntChannel.setRfFrequency(CHANNEL_POWER_FREQUENCY); + mAntChannel.open(); + mIsOpen = true; + + Log.d(TAG, "Opened channel with device number: " + POWER_SENSOR_ID); + } catch (RemoteException e) { + channelError(e); + } catch (AntCommandFailedException e) { + // This will release, and therefore unassign if required + channelError("Open failed", e); + } + } + } else { + Log.w(TAG, "No channel available"); + } + + return mIsOpen; + } + + + void channelError(RemoteException e) { + String logString = "Remote service communication failed."; + + Log.e(TAG, logString); + + } + + void channelError(String error, AntCommandFailedException e) { + StringBuilder logString; + + if (e.getResponseMessage() != null) { + String initiatingMessageId = "0x" + Integer.toHexString( + e.getResponseMessage().getInitiatingMessageId()); + String rawResponseCode = "0x" + Integer.toHexString( + e.getResponseMessage().getRawResponseCode()); + + logString = new StringBuilder(error) + .append(". Command ") + .append(initiatingMessageId) + .append(" failed with code ") + .append(rawResponseCode); + } else { + String attemptedMessageId = "0x" + Integer.toHexString( + e.getAttemptedMessageType().getMessageId()); + String failureReason = e.getFailureReason().toString(); + + logString = new StringBuilder(error) + .append(". Command ") + .append(attemptedMessageId) + .append(" failed with reason ") + .append(failureReason); + } + + Log.e(TAG, logString.toString()); + + mAntChannel.release(); + } + + public void close() { + // TODO kill all our resources + if (null != mAntChannel) { + mIsOpen = false; + + // Releasing the channel to make it available for others. + // After releasing, the AntChannel instance cannot be reused. + mAntChannel.release(); + mAntChannel = null; + } + + Log.e(TAG, "Channel Closed"); + } + + /** + * Implements the Channel Event Handler Interface so that messages can be + * received and channel death events can be handled. + */ + public class ChannelEventCallback implements IAntChannelEventHandler { + + int cnt = 0; + int eventCount = 0; + int cumulativePower = 0; + + @Override + public void onChannelDeath() { + // Display channel death message when channel dies + Log.e(TAG, "Channel Death"); + } + + @Override + public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) { + Log.d(TAG, "Rx: " + antParcel); + Log.d(TAG, "Message Type: " + messageType); + byte[] payload = new byte[8]; + + // Switching on message type to handle different types of messages + switch (messageType) { + // If data message, construct from parcel and update channel data + case BROADCAST_DATA: + // Rx Data + //updateData(new BroadcastDataMessage(antParcel).getPayload()); + break; + case ACKNOWLEDGED_DATA: + // Rx Data + //updateData(new AcknowledgedDataMessage(antParcel).getPayload()); + payload = new AcknowledgedDataMessage(antParcel).getPayload(); + Log.d(TAG, "AcknowledgedDataMessage: " + payload); + + if (payload[0] == 0 && payload[1] == 1 && payload[2] == 0xAA) { + payload[0] = (byte) 0x01; + payload[1] = (byte) 0xAC; + payload[2] = (byte) 0xFF; + payload[3] = (byte) 0xFF; + payload[4] = (byte) 0xFF; + payload[5] = (byte) 0xFF; + payload[6] = (byte) 0x00; + payload[7] = (byte) 0x00; + try { + // Setting the data to be broadcast on the next channel period + mAntChannel.setBroadcastData(payload); + } catch (RemoteException e) { + channelError(e); + } + } + break; + case CHANNEL_EVENT: + // Constructing channel event message from parcel + ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel); + EventCode code = eventMessage.getEventCode(); + Log.d(TAG, "Event Code: " + code); + + // Switching on event code to handle the different types of channel events + switch (code) { + case TX: + cnt += 1; + + if (cnt % 61 == 15) { + payload[0] = (byte) 0x50; + payload[1] = (byte) 0xFF; + payload[2] = (byte) 0xFF; + payload[3] = (byte) 0x01; + payload[4] = (byte) 0xFF; + payload[5] = (byte) 0x00; + payload[6] = (byte) 0x01; + payload[7] = (byte) 0x00; + } else if (cnt % 61 == 30) { + payload[0] = (byte) 0x51; + payload[1] = (byte) 0xFF; + payload[2] = (byte) 0xFF; + payload[3] = (byte) 0x01; + payload[4] = (byte) ((POWER_SENSOR_ID) & 0xFF); + payload[5] = (byte) ((POWER_SENSOR_ID >> 8) & 0xFF); + payload[6] = (byte) ((POWER_SENSOR_ID >> 16) & 0xFF); + payload[7] = (byte) ((POWER_SENSOR_ID >> 24) & 0xFF); + } else { + eventCount = (eventCount + 1) & 0xFF; + cumulativePower = (cumulativePower + power) & 0xFFFF; + payload[0] = (byte) 0x10; + payload[1] = (byte) eventCount; + payload[2] = (byte) 0xFF; + payload[3] = (byte) cadence; + payload[4] = (byte) ((cumulativePower) & 0xFF); + payload[5] = (byte) ((cumulativePower >> 8) & 0xFF); + payload[6] = (byte) ((power) & 0xFF); + payload[7] = (byte) ((power >> 8) & 0xFF); + } + + if (mIsOpen) { + try { + // Setting the data to be broadcast on the next channel period + mAntChannel.setBroadcastData(payload); + } catch (RemoteException e) { + channelError(e); + } + } + break; + case CHANNEL_COLLISION: + cnt += 1; + break; + case RX_SEARCH_TIMEOUT: + // TODO May want to keep searching + Log.e(TAG, "No Device Found"); + break; + case CHANNEL_CLOSED: + case RX_FAIL: + case RX_FAIL_GO_TO_SEARCH: + case TRANSFER_RX_FAILED: + case TRANSFER_TX_COMPLETED: + case TRANSFER_TX_FAILED: + case TRANSFER_TX_START: + case UNKNOWN: + // TODO More complex communication will need to handle these events + break; + } + break; + case ANT_VERSION: + case BURST_TRANSFER_DATA: + case CAPABILITIES: + case CHANNEL_ID: + case CHANNEL_RESPONSE: + case CHANNEL_STATUS: + case SERIAL_NUMBER: + case OTHER: + // TODO More complex communication will need to handle these message types + break; + } + } + } +} diff --git a/Application/src/main/java/org/surfsite/iconsole/SpeedChannelController.java b/Application/src/main/java/org/surfsite/iconsole/SpeedChannelController.java new file mode 100644 index 0000000..7a83b33 --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/SpeedChannelController.java @@ -0,0 +1,306 @@ +/* + * Copyright 2012 Dynastream Innovations Inc. + * + * 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 org.surfsite.iconsole; + +import android.os.RemoteException; +import android.util.Log; + +import com.dsi.ant.channel.AntChannel; +import com.dsi.ant.channel.AntCommandFailedException; +import com.dsi.ant.channel.IAntChannelEventHandler; +import com.dsi.ant.message.ChannelId; +import com.dsi.ant.message.ChannelType; +import com.dsi.ant.message.EventCode; +import com.dsi.ant.message.fromant.ChannelEventMessage; +import com.dsi.ant.message.fromant.MessageFromAntType; +import com.dsi.ant.message.ipc.AntMessageParcel; + +import java.util.Random; + +public class SpeedChannelController { + // The device type and transmission type to be part of the channel ID message + private static final int CHANNEL_SPEED_DEVICE_TYPE = 0x7B; + private static final int CHANNEL_SPEED_TRANSMISSION_TYPE = 1; + + // The period and frequency values the channel will be configured to + private static final int CHANNEL_SPEED_PERIOD = 8118; // 1 Hz + private static final int CHANNEL_SPEED_FREQUENCY = 57; + + private static final String TAG = SpeedChannelController.class.getSimpleName(); + public static final int SPEED_SENSOR_ID = 0x9e3d4b65; + + private static Random randGen = new Random(); + + private AntChannel mAntChannel; + + private ChannelEventCallback mChannelEventCallback = new ChannelEventCallback(); + + + private boolean mIsOpen; + double speed = 0.0; + + public SpeedChannelController(AntChannel antChannel) { + mAntChannel = antChannel; + openChannel(); + } + + boolean openChannel() { + if (null != mAntChannel) { + if (mIsOpen) { + Log.w(TAG, "Channel was already open"); + } else { + // Channel ID message contains device number, type and transmission type. In + // order for master (TX) channels and slave (RX) channels to connect, they + // must have the same channel ID, or wildcard (0) is used. + ChannelId channelId = new ChannelId(SPEED_SENSOR_ID & 0xFFFF, + CHANNEL_SPEED_DEVICE_TYPE, CHANNEL_SPEED_TRANSMISSION_TYPE); + + try { + // Setting the channel event handler so that we can receive messages from ANT + mAntChannel.setChannelEventHandler(mChannelEventCallback); + + // Performs channel assignment by assigning the type to the channel. Additional + // features (such as, background scanning and frequency agility) can be enabled + // by passing an ExtendedAssignment object to assign(ChannelType, ExtendedAssignment). + mAntChannel.assign(ChannelType.BIDIRECTIONAL_MASTER); + + /* + * Configures the channel ID, messaging period and rf frequency after assigning, + * then opening the channel. + * + * For any additional ANT features such as proximity search or background scanning, refer to + * the ANT Protocol Doc found at: + * http://www.thisisant.com/resources/ant-message-protocol-and-usage/ + */ + mAntChannel.setChannelId(channelId); + mAntChannel.setPeriod(CHANNEL_SPEED_PERIOD); + mAntChannel.setRfFrequency(CHANNEL_SPEED_FREQUENCY); + mAntChannel.open(); + mIsOpen = true; + + Log.d(TAG, "Opened channel with device number: " + SPEED_SENSOR_ID); + } catch (RemoteException e) { + channelError(e); + } catch (AntCommandFailedException e) { + // This will release, and therefore unassign if required + channelError("Open failed", e); + } + } + } else { + Log.w(TAG, "No channel available"); + } + + return mIsOpen; + } + + void channelError(RemoteException e) { + String logString = "Remote service communication failed."; + + Log.e(TAG, logString); + } + + void channelError(String error, AntCommandFailedException e) { + StringBuilder logString; + + if (e.getResponseMessage() != null) { + String initiatingMessageId = "0x" + Integer.toHexString( + e.getResponseMessage().getInitiatingMessageId()); + String rawResponseCode = "0x" + Integer.toHexString( + e.getResponseMessage().getRawResponseCode()); + + logString = new StringBuilder(error) + .append(". Command ") + .append(initiatingMessageId) + .append(" failed with code ") + .append(rawResponseCode); + } else { + String attemptedMessageId = "0x" + Integer.toHexString( + e.getAttemptedMessageType().getMessageId()); + String failureReason = e.getFailureReason().toString(); + + logString = new StringBuilder(error) + .append(". Command ") + .append(attemptedMessageId) + .append(" failed with reason ") + .append(failureReason); + } + + Log.e(TAG, logString.toString()); + + mAntChannel.release(); + + Log.e(TAG, "ANT Command Failed"); + } + + public void close() { + // TODO kill all our resources + if (null != mAntChannel) { + mIsOpen = false; + + // Releasing the channel to make it available for others. + // After releasing, the AntChannel instance cannot be reused. + mAntChannel.release(); + mAntChannel = null; + } + + Log.e(TAG, "Channel Closed"); + } + + /** + * Implements the Channel Event Handler Interface so that messages can be + * received and channel death events can be handled. + */ + public class ChannelEventCallback implements IAntChannelEventHandler { + int revCounts = 0; + int ucMessageCount = 0; + byte ucPageChange = 0; + byte ucExtMesgType = 1; + long lastTime = 0; + double way; + int rev; + double remWay; + double wheel = 0.1; + + @Override + public void onChannelDeath() { + // Display channel death message when channel dies + Log.e(TAG, "Channel Death"); + } + + @Override + public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) { + Log.d(TAG, "Rx: " + antParcel); + Log.d(TAG, "Message Type: " + messageType); + + // Switching on message type to handle different types of messages + switch (messageType) { + // If data message, construct from parcel and update channel data + case BROADCAST_DATA: + // Rx Data + //updateData(new BroadcastDataMessage(antParcel).getPayload()); + break; + case ACKNOWLEDGED_DATA: + // Rx Data + //updateData(new AcknowledgedDataMessage(antParcel).getPayload()); + break; + case CHANNEL_EVENT: + // Constructing channel event message from parcel + ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel); + EventCode code = eventMessage.getEventCode(); + Log.d(TAG, "Event Code: " + code); + + // Switching on event code to handle the different types of channel events + switch (code) { + case TX: + long unixTime = System.currentTimeMillis() / 1000L; + + if (lastTime != 0) { + way = speed * (unixTime - lastTime) / 3.6 + remWay; + rev = (int)(way / wheel + 0.5); + remWay = way - rev * wheel; + revCounts += rev; + } + lastTime = unixTime; + + ucPageChange += 0x20; + ucPageChange &= 0xF0; + ucMessageCount += 1; + byte[] payload = new byte[8]; + + if (ucMessageCount >= 65) { + if (ucExtMesgType >= 4) + ucExtMesgType = 1; + + if (ucExtMesgType == 1) { + int halfunixTime = (int) (unixTime / 2L); + payload[0] = (byte) ((byte) 0x01 | (byte) (ucPageChange & (byte) 0x80)); + payload[1] = (byte) (halfunixTime & 0xFF); + payload[2] = (byte) ((halfunixTime >> 8) & 0xFF); + payload[3] = (byte) ((halfunixTime >> 16) & 0xFF); + } + else if (ucExtMesgType == 2) { + payload[0] = (byte) ((byte) 0x02 | (byte) (ucPageChange & (byte) 0x80)); + payload[1] = (byte) 0xFF; + payload[2] = (byte) ((SPEED_SENSOR_ID >> 16) & 0xFF); + payload[3] = (byte) ((SPEED_SENSOR_ID >> 24) & 0xFF); + } + else if (ucExtMesgType == 3) { + payload[0] = (byte) ((byte) 0x03 | (byte) (ucPageChange & (byte) 0x80)); + payload[1] = (byte) 0x01; + payload[2] = (byte) 0x01; + payload[3] = (byte) 0x01; + } + if (ucMessageCount >= 68) { + ucMessageCount = 0; + ucExtMesgType += 1; + } + } else { + payload[0] = (byte) (ucPageChange & 0x80); + payload[1] = (byte) 0xFF; + payload[2] = (byte) 0xFF; + payload[3] = (byte) 0xFF; + } + + int unixTime1024 = (int) (unixTime * 1024); + payload[4] = (byte) (unixTime1024 & 0xFF); + payload[5] = (byte) ((unixTime1024 >> 8) & 0xFF); + payload[6] = (byte) (revCounts & 0xFF); + payload[7] = (byte) ((revCounts >> 8) & 0xFF); + + if (mIsOpen) { + try { + // Setting the data to be broadcast on the next channel period + mAntChannel.setBroadcastData(payload); + } catch (RemoteException e) { + channelError(e); + } + } + break; + case CHANNEL_COLLISION: + ucPageChange += 0x20; + ucPageChange &= 0xF0; + ucMessageCount += 1; + break; + case RX_SEARCH_TIMEOUT: + // TODO May want to keep searching + Log.e(TAG, "No Device Found"); + break; + case CHANNEL_CLOSED: + case RX_FAIL: + case RX_FAIL_GO_TO_SEARCH: + case TRANSFER_RX_FAILED: + case TRANSFER_TX_COMPLETED: + case TRANSFER_TX_FAILED: + case TRANSFER_TX_START: + case UNKNOWN: + // TODO More complex communication will need to handle these events + break; + } + break; + case ANT_VERSION: + case BURST_TRANSFER_DATA: + case CAPABILITIES: + case CHANNEL_ID: + case CHANNEL_RESPONSE: + case CHANNEL_STATUS: + case SERIAL_NUMBER: + case OTHER: + // TODO More complex communication will need to handle these message types + break; + } + } + } +} diff --git a/android_antlib_4-14/android_antlib_4-14.iml b/android_antlib_4-14/android_antlib_4-14.iml new file mode 100644 index 0000000..18306c8 --- /dev/null +++ b/android_antlib_4-14/android_antlib_4-14.iml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android_antlib_4-14/build.gradle b/android_antlib_4-14/build.gradle new file mode 100644 index 0000000..04de0a9 --- /dev/null +++ b/android_antlib_4-14/build.gradle @@ -0,0 +1,2 @@ +configurations.maybeCreate("default") +artifacts.add("default", file('android_antlib_4-14-0.jar')) \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 68c02fb..7f179be 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1 @@ - - - -include 'Application' +include 'Application', ':android_antlib_4-14' From 3f58a7d10db9d5d15b40427296ad49869418f139 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Mon, 9 Jul 2018 14:06:24 +0200 Subject: [PATCH 2/9] update --- .idea/misc.xml | 2 +- Application/Application.iml | 1 - .../iconsole/BluetoothChatFragment.java | 285 +++++++++--------- .../iconsole/BluetoothChatService.java | 83 ++--- android_antlib_4-14/android_antlib_4-14.iml | 1 + 5 files changed, 180 insertions(+), 192 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 3b6674f..5bae4e2 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -24,7 +24,7 @@ - + diff --git a/Application/Application.iml b/Application/Application.iml index 153210c..cf59ce6 100644 --- a/Application/Application.iml +++ b/Application/Application.iml @@ -91,7 +91,6 @@ - diff --git a/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java b/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java index 1746361..8087e1f 100644 --- a/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java +++ b/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java @@ -32,6 +32,7 @@ import android.os.Message; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -42,8 +43,6 @@ import android.widget.Button; import android.widget.NumberPicker; import android.widget.TextView; import android.widget.Toast; -import android.util.Log; -import android.content.Intent; /** * This fragment controls Bluetooth to communicate with other devices. @@ -76,7 +75,7 @@ public class BluetoothChatFragment extends Fragment { /** * Array adapter for the conversation thread - private ArrayAdapter mConversationArrayAdapter; + private ArrayAdapter mConversationArrayAdapter; */ /** @@ -90,7 +89,132 @@ public class BluetoothChatFragment extends Fragment { private BluetoothChatService mChatService = null; private boolean mIsBound; private ChannelService.ChannelServiceComm mChannelService; + /** + * The Handler that gets information back from the BluetoothChatService + */ + private final Handler mHandler = new Handler() { + @SuppressLint("DefaultLocale") + @Override + public void handleMessage(Message msg) { + FragmentActivity activity = getActivity(); + switch (msg.what) { + case Constants.MESSAGE_STATE_CHANGE: + switch (msg.arg1) { + case BluetoothChatService.STATE_CONNECTED: + setStatus(getString(R.string.title_connected_to, mConnectedDeviceName)); + //mConversationArrayAdapter.clear(); + mStartButton.setEnabled(true); + mStopButton.setEnabled(true); + mDisconnectButton.setEnabled(true); + mLevel.setEnabled(true); + mLevel.setValue(1); + break; + case BluetoothChatService.STATE_CONNECTING: + setStatus(R.string.title_connecting); + mStartButton.setEnabled(false); + mStopButton.setEnabled(false); + mDisconnectButton.setEnabled(false); + mLevel.setEnabled(false); + break; + case BluetoothChatService.STATE_LISTEN: + case BluetoothChatService.STATE_NONE: + setStatus(R.string.title_not_connected); + mStartButton.setEnabled(false); + mStopButton.setEnabled(false); + mDisconnectButton.setEnabled(false); + mLevel.setEnabled(false); + break; + } + break; + case Constants.MESSAGE_DATA: + if (!(msg.obj instanceof IConsole.Data)) + return; + IConsole.Data data = (IConsole.Data) msg.obj; + mChannelService.setSpeed(data.mSpeed10 / 10.0); + mChannelService.setPower(data.mPower10 / 10); + mChannelService.setCadence(data.mRPM); + + mSpeedText.setText(String.format("% 3.1f", data.mSpeed10 / 10.0)); + mPowerText.setText(String.format("% 3.1f", data.mPower10 / 10.0)); + mRPMText.setText(String.format("%d", data.mRPM)); + mDistanceText.setText(String.format("% 3.1f", data.mDistance10 / 10.0)); + mCaloriesText.setText(String.format("% 3d", data.mCalories)); + mHFText.setText(String.format("%d", data.mHF)); + mTimeText.setText(String.format("%s", data.getTimeStr())); + //mLevel.setValue(data.mLevel); + break; + case Constants.MESSAGE_WRITE: + //byte[] writeBuf = (byte[]) msg.obj; + // construct a string from the buffer + //String writeMessage = new String(writeBuf); + //mConversationArrayAdapter.add("Me: " + writeMessage); + break; + case Constants.MESSAGE_READ: + //byte[] readBuf = (byte[]) msg.obj; + // construct a string from the valid bytes in the buffer + //String readMessage = new String(readBuf, 0, msg.arg1); + //mConversationArrayAdapter.add(mConnectedDeviceName + ": " + readMessage); + break; + case Constants.MESSAGE_DEVICE_NAME: + // save the connected device's name + mConnectedDeviceName = msg.getData().getString(Constants.DEVICE_NAME); + if (null != activity) { + Toast.makeText(activity, "Connected to " + + mConnectedDeviceName, Toast.LENGTH_SHORT).show(); + } + break; + case Constants.MESSAGE_TOAST: + if (null != activity) { + Toast.makeText(activity, msg.getData().getString(Constants.TOAST), + Toast.LENGTH_SHORT).show(); + } + break; + } + } + }; private boolean mChannelServiceBound = false; + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + // This is called when the connection with the service has been + // established, giving us the service object we can use to + // interact with the service. Because we have bound to a explicit + // service that we know is running in our own process, we can + // cast its IBinder to a concrete class and directly access it. + mChatService = ((BluetoothChatService.BluetoothChatServiceI) service).getService(); + ((BluetoothChatService.BluetoothChatServiceI) service).setHandler(mHandler); + Log.d(TAG, "onServiceConnected()"); + } + + public void onServiceDisconnected(ComponentName className) { + // This is called when the connection with the service has been + // unexpectedly disconnected -- that is, its process crashed. + // Because it is running in our same process, we should never + // see this happen. + mChatService = null; + + } + }; + private ServiceConnection mChannelServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder serviceBinder) { + Log.v(TAG, "mChannelServiceConnection.onServiceConnected..."); + + mChannelService = (ChannelService.ChannelServiceComm) serviceBinder; + + + Log.v(TAG, "...mChannelServiceConnection.onServiceConnected"); + } + + @Override + public void onServiceDisconnected(ComponentName arg0) { + Log.v(TAG, "mChannelServiceConnection.onServiceDisconnected..."); + + // Clearing and disabling when disconnecting from ChannelService + mChannelService = null; + + Log.v(TAG, "...mChannelServiceConnection.onServiceDisconnected"); + } + }; @Override public void onCreate(Bundle savedInstanceState) { @@ -107,7 +231,6 @@ public class BluetoothChatFragment extends Fragment { } } - @Override public void onStart() { super.onStart(); @@ -120,7 +243,7 @@ public class BluetoothChatFragment extends Fragment { } else if (mChatService == null) { setupChat(); } - if(!mChannelServiceBound) doBindChannelService(); + if (!mChannelServiceBound) doBindChannelService(); } @@ -180,28 +303,6 @@ public class BluetoothChatFragment extends Fragment { mTimeText = (TextView) view.findViewById(R.id.Time); } - private ServiceConnection mConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder service) { - // This is called when the connection with the service has been - // established, giving us the service object we can use to - // interact with the service. Because we have bound to a explicit - // service that we know is running in our own process, we can - // cast its IBinder to a concrete class and directly access it. - mChatService = ((BluetoothChatService.BluetoothChatServiceI)service).getService(); - ((BluetoothChatService.BluetoothChatServiceI)service).setHandler(mHandler); - Log.d(TAG, "onServiceConnected()"); - } - - public void onServiceDisconnected(ComponentName className) { - // This is called when the connection with the service has been - // unexpectedly disconnected -- that is, its process crashed. - // Because it is running in our same process, we should never - // see this happen. - mChatService = null; - - } - }; - void doBindService() { Log.d(TAG, "doBindService()"); @@ -209,7 +310,7 @@ public class BluetoothChatFragment extends Fragment { // class name because we want a specific service implementation that // we know will be running in our own process (and thus won't be // supporting component replacement by other applications). - getActivity().bindService(new Intent(getActivity(), BluetoothChatService.class), mConnection , Context.BIND_AUTO_CREATE); + getActivity().bindService(new Intent(getActivity(), BluetoothChatService.class), mConnection, Context.BIND_AUTO_CREATE); mIsBound = true; } @@ -221,53 +322,25 @@ public class BluetoothChatFragment extends Fragment { } } - private ServiceConnection mChannelServiceConnection = new ServiceConnection() - { - @Override - public void onServiceConnected(ComponentName name, IBinder serviceBinder) - { - Log.v(TAG, "mChannelServiceConnection.onServiceConnected..."); - - mChannelService = (ChannelService.ChannelServiceComm) serviceBinder; - - - Log.v(TAG, "...mChannelServiceConnection.onServiceConnected"); - } - - @Override - public void onServiceDisconnected(ComponentName arg0) - { - Log.v(TAG, "mChannelServiceConnection.onServiceDisconnected..."); - - // Clearing and disabling when disconnecting from ChannelService - mChannelService = null; - - Log.v(TAG, "...mChannelServiceConnection.onServiceDisconnected"); - } - }; - - private void doBindChannelService() - { + private void doBindChannelService() { Log.v(TAG, "doBindChannelService..."); // Binds to ChannelService. ChannelService binds and manages connection between the // app and the ANT Radio Service - mChannelServiceBound = getActivity().bindService(new Intent(getActivity(), ChannelService.class), mChannelServiceConnection , Context.BIND_AUTO_CREATE); + mChannelServiceBound = getActivity().bindService(new Intent(getActivity(), ChannelService.class), mChannelServiceConnection, Context.BIND_AUTO_CREATE); - if(!mChannelServiceBound) //If the bind returns false, run the unbind method to update the GUI + if (!mChannelServiceBound) //If the bind returns false, run the unbind method to update the GUI doUnbindChannelService(); - Log.i(TAG, " Channel Service binding = "+ mChannelServiceBound); + Log.i(TAG, " Channel Service binding = " + mChannelServiceBound); Log.v(TAG, "...doBindChannelService"); } - private void doUnbindChannelService() - { + private void doUnbindChannelService() { Log.v(TAG, "doUnbindChannelService..."); - if(mChannelServiceBound) - { + if (mChannelServiceBound) { getActivity().unbindService(mChannelServiceConnection); mChannelServiceBound = false; @@ -276,7 +349,6 @@ public class BluetoothChatFragment extends Fragment { Log.v(TAG, "...doUnbindChannelService"); } - /** * Set up the UI and background operations for chat. */ @@ -351,7 +423,7 @@ public class BluetoothChatFragment extends Fragment { if (mChatService == null) return; - // Check that we're actually connected before trying anything + // Check that we're actually connected before trying anything if (mChatService.getState() != BluetoothChatService.STATE_CONNECTED) { Toast.makeText(getActivity(), R.string.not_connected, Toast.LENGTH_SHORT).show(); return; @@ -394,91 +466,6 @@ public class BluetoothChatFragment extends Fragment { actionBar.setSubtitle(subTitle); } - - /** - * The Handler that gets information back from the BluetoothChatService - */ - private final Handler mHandler = new Handler() { - @SuppressLint("DefaultLocale") - @Override - public void handleMessage(Message msg) { - FragmentActivity activity = getActivity(); - switch (msg.what) { - case Constants.MESSAGE_STATE_CHANGE: - switch (msg.arg1) { - case BluetoothChatService.STATE_CONNECTED: - setStatus(getString(R.string.title_connected_to, mConnectedDeviceName)); - //mConversationArrayAdapter.clear(); - mStartButton.setEnabled(true); - mStopButton.setEnabled(true); - mDisconnectButton.setEnabled(true); - mLevel.setEnabled(true); - mLevel.setValue(1); - break; - case BluetoothChatService.STATE_CONNECTING: - setStatus(R.string.title_connecting); - mStartButton.setEnabled(false); - mStopButton.setEnabled(false); - mDisconnectButton.setEnabled(false); - mLevel.setEnabled(false); - break; - case BluetoothChatService.STATE_LISTEN: - case BluetoothChatService.STATE_NONE: - setStatus(R.string.title_not_connected); - mStartButton.setEnabled(false); - mStopButton.setEnabled(false); - mDisconnectButton.setEnabled(false); - mLevel.setEnabled(false); - break; - } - break; - case Constants.MESSAGE_DATA: - if (!(msg.obj instanceof IConsole.Data)) - return; - IConsole.Data data = (IConsole.Data) msg.obj; - mChannelService.setSpeed(data.mSpeed10 / 10.0); - mChannelService.setPower(data.mPower10 / 10); - mChannelService.setCadence(data.mRPM); - - mSpeedText.setText(String.format("% 3.1f", data.mSpeed10 / 10.0)); - mPowerText.setText(String.format("% 3.1f", data.mPower10 / 10.0)); - mRPMText.setText(String.format("%d", data.mRPM)); - mDistanceText.setText(String.format("% 3.1f", data.mDistance10 / 10.0)); - mCaloriesText.setText(String.format("% 3d", data.mCalories)); - mHFText.setText(String.format("%d", data.mHF)); - mTimeText.setText(String.format("%s",data.getTimeStr())); - //mLevel.setValue(data.mLevel); - break; - case Constants.MESSAGE_WRITE: - //byte[] writeBuf = (byte[]) msg.obj; - // construct a string from the buffer - //String writeMessage = new String(writeBuf); - //mConversationArrayAdapter.add("Me: " + writeMessage); - break; - case Constants.MESSAGE_READ: - //byte[] readBuf = (byte[]) msg.obj; - // construct a string from the valid bytes in the buffer - //String readMessage = new String(readBuf, 0, msg.arg1); - //mConversationArrayAdapter.add(mConnectedDeviceName + ": " + readMessage); - break; - case Constants.MESSAGE_DEVICE_NAME: - // save the connected device's name - mConnectedDeviceName = msg.getData().getString(Constants.DEVICE_NAME); - if (null != activity) { - Toast.makeText(activity, "Connected to " - + mConnectedDeviceName, Toast.LENGTH_SHORT).show(); - } - break; - case Constants.MESSAGE_TOAST: - if (null != activity) { - Toast.makeText(activity, msg.getData().getString(Constants.TOAST), - Toast.LENGTH_SHORT).show(); - } - break; - } - } - }; - public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case REQUEST_CONNECT_DEVICE_SECURE: diff --git a/Application/src/main/java/org/surfsite/iconsole/BluetoothChatService.java b/Application/src/main/java/org/surfsite/iconsole/BluetoothChatService.java index 8bd3b96..1085fc7 100644 --- a/Application/src/main/java/org/surfsite/iconsole/BluetoothChatService.java +++ b/Application/src/main/java/org/surfsite/iconsole/BluetoothChatService.java @@ -23,14 +23,12 @@ import android.app.Service; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; -import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; - import android.util.Log; import java.io.IOException; @@ -45,32 +43,32 @@ import java.util.UUID; * thread for performing data transmissions when connected. */ public class BluetoothChatService extends Service { - // Debugging - private static final String TAG = "BluetoothChatService"; - // Name for the SDP record when creating server socket - private static final String NAME_SECURE = "BluetoothChatSecure"; - private static final String NAME_INSECURE = "BluetoothChatInsecure"; - - // Unique UUID for this application - private static final UUID SERIAL_PORT_CLASS = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb"); - // Member fields - private final BluetoothAdapter mAdapter; - private Handler mHandler; - - private ConnectThread mConnectThread; - private ConnectedThread mConnectedThread; - private int mState; - private int mNewState; - // Constants that indicate the current connection state public static final int STATE_NONE = 0; // we're doing nothing public static final int STATE_LISTEN = 1; // now listening for incoming connections public static final int STATE_CONNECTING = 2; // now initiating an outgoing connection public static final int STATE_CONNECTED = 3; // now connected to a remote device + // Debugging + private static final String TAG = "BluetoothChatService"; + // Name for the SDP record when creating server socket + private static final String NAME_SECURE = "BluetoothChatSecure"; + private static final String NAME_INSECURE = "BluetoothChatInsecure"; + // Unique UUID for this application + private static final UUID SERIAL_PORT_CLASS = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb"); + // Member fields + private final BluetoothAdapter mAdapter; + // This is the object that receives interactions from clients. See + // RemoteService for a more complete example. + private final IBinder mBinder = new BluetoothChatServiceI(); + private Handler mHandler; + private ConnectThread mConnectThread; + private ConnectedThread mConnectedThread; + private int mState; + private int mNewState; private NotificationManager mNM; - private int NOTIFICATION = R.string.local_service_started; + public BluetoothChatService() { super(); mAdapter = BluetoothAdapter.getDefaultAdapter(); @@ -78,21 +76,11 @@ public class BluetoothChatService extends Service { mNewState = mState; } - - public class BluetoothChatServiceI extends Binder { - BluetoothChatService getService() { - return BluetoothChatService.this; - } - void setHandler(Handler handler) { - mHandler = handler; - } - } - @Override public void onCreate() { super.onCreate(); - mNM = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); + mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); } @@ -144,11 +132,6 @@ public class BluetoothChatService extends Service { return mBinder; } - - // This is the object that receives interactions from clients. See - // RemoteService for a more complete example. - private final IBinder mBinder = new BluetoothChatServiceI(); - /** * Update UI title according to the current state of the chat connection */ @@ -186,6 +169,7 @@ public class BluetoothChatService extends Service { super.onDestroy(); } + /** * Show a notification while this service is running. */ @@ -288,7 +272,6 @@ public class BluetoothChatService extends Service { updateUserInterfaceTitle(); } - /** * Indicate that the connection attempt failed and notify the UI Activity. */ @@ -327,12 +310,22 @@ public class BluetoothChatService extends Service { BluetoothChatService.this.startBT(); } + public class BluetoothChatServiceI extends Binder { + BluetoothChatService getService() { + return BluetoothChatService.this; + } + + void setHandler(Handler handler) { + mHandler = handler; + } + } + /** * This thread runs while attempting to make an outgoing connection * with a device. It runs straight through; the connection either * succeeds or fails. */ - class ConnectThread extends Thread { + class ConnectThread extends Thread { private final BluetoothSocket mmSocket; private final BluetoothDevice mmDevice; @@ -481,9 +474,17 @@ public class BluetoothChatService extends Service { } - public boolean setLevel(int level) { return mmIConsole.setLevel(level); } - public boolean startIConsole() { return mmIConsole.start(); } - public boolean stopIConsole() { return mmIConsole.stop(); } + public boolean setLevel(int level) { + return mmIConsole.setLevel(level); + } + + public boolean startIConsole() { + return mmIConsole.start(); + } + + public boolean stopIConsole() { + return mmIConsole.stop(); + } public void cancel() { mmIConsole.stop(); diff --git a/android_antlib_4-14/android_antlib_4-14.iml b/android_antlib_4-14/android_antlib_4-14.iml index 18306c8..47b5059 100644 --- a/android_antlib_4-14/android_antlib_4-14.iml +++ b/android_antlib_4-14/android_antlib_4-14.iml @@ -21,5 +21,6 @@ + \ No newline at end of file From 326b3576ffb980aed34ae90d27443543337978a5 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Mon, 9 Jul 2018 15:15:55 +0200 Subject: [PATCH 3/9] fixed AcknowledgedDataMessage --- Application/Application.iml | 1 + .../main/java/org/surfsite/iconsole/PowerChannelController.java | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Application/Application.iml b/Application/Application.iml index cf59ce6..153210c 100644 --- a/Application/Application.iml +++ b/Application/Application.iml @@ -91,6 +91,7 @@ + diff --git a/Application/src/main/java/org/surfsite/iconsole/PowerChannelController.java b/Application/src/main/java/org/surfsite/iconsole/PowerChannelController.java index d678426..081c641 100644 --- a/Application/src/main/java/org/surfsite/iconsole/PowerChannelController.java +++ b/Application/src/main/java/org/surfsite/iconsole/PowerChannelController.java @@ -189,7 +189,7 @@ public class PowerChannelController { payload = new AcknowledgedDataMessage(antParcel).getPayload(); Log.d(TAG, "AcknowledgedDataMessage: " + payload); - if (payload[0] == 0 && payload[1] == 1 && payload[2] == 0xAA) { + if ((payload[0] == 0) && (payload[1] == 1) && (payload[2] == (byte)0xAA)) { payload[0] = (byte) 0x01; payload[1] = (byte) 0xAC; payload[2] = (byte) 0xFF; From 5dc2a1244950b4a4bb24de136e37f94d4510b49a Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Sun, 12 May 2019 15:58:02 +0200 Subject: [PATCH 4/9] update --- Application/build.gradle | 13 +++++++------ build.gradle | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Application/build.gradle b/Application/build.gradle index 8f87031..ebcfc8a 100644 --- a/Application/build.gradle +++ b/Application/build.gradle @@ -6,10 +6,11 @@ buildscript { url 'https://maven.google.com/' name 'Google' } + google() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.3' + classpath 'com.android.tools.build:gradle:3.4.0' } } @@ -24,11 +25,11 @@ repositories { } dependencies { - implementation "com.android.support:gridlayout-v7:27.1.1" - implementation "com.android.support:cardview-v7:27.1.1" - implementation "com.android.support:appcompat-v7:27.1.1" - implementation 'com.android.support:support-v13:27.1.1' - implementation 'com.android.support.constraint:constraint-layout:1.1.2' + implementation "com.android.support:gridlayout-v7:28.0.0" + implementation "com.android.support:cardview-v7:28.0.0" + implementation "com.android.support:appcompat-v7:28.0.0" + implementation 'com.android.support:support-v13:28.0.0' + implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation project(':android_antlib_4-14') } diff --git a/build.gradle b/build.gradle index ad6a928..7487467 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.3' + classpath 'com.android.tools.build:gradle:3.4.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From 2da8828ef17afe37c1ef1e5c445ff119291ccb57 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Sun, 12 May 2019 15:59:03 +0200 Subject: [PATCH 5/9] update gradle --- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index adde34a..17ebf47 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jul 06 15:08:55 CEST 2018 +#Sun May 12 14:24:25 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip From a7623d0662bea56cdef2f87458ede06da46334de Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Wed, 27 Jan 2021 13:15:33 +0100 Subject: [PATCH 6/9] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 324c670..13f4a3c 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,6 @@ Contribute via pull requests. If you want to do major contributions, drop me a m - workout recording to FIT/TCX/GPX - workout profiles - Google street view cycling -- sensor data broadcasting via ANT +- sensor data broadcasting via ANT (works, message me for a howto or look [here](https://github.com/haraldh/iconsole-android/issues/4)) See also my [python prototype](https://github.com/haraldh/iconsole) for the bluetooth protocol reverse engineering and sensor broadcasting. From b1c1a0cb1f9a240178b19c7d458728b372831792 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Wed, 26 May 2021 16:32:45 +0200 Subject: [PATCH 7/9] iii --- .idea/caches/build_file_checksums.ser | Bin 0 -> 605 bytes .idea/caches/gradle_models.ser | Bin 0 -> 130606 bytes .idea/codeStyles/Project.xml | 116 +++++++ .idea/compiler.xml | 8 +- .idea/gradle.xml | 4 + .idea/jarRepositories.xml | 30 ++ .idea/misc.xml | 27 +- .idea/modules.xml | 4 +- .../iconsole-android.Application.iml | 83 +++++ .../iconsole-android.android_antlib_4-14.iml | 28 ++ Application/Application.iml | 145 -------- Application/build.gradle | 2 +- Application/src/main/AndroidManifest.xml | 8 +- .../iconsole/BluetoothChatFragment.java | 174 +++++----- .../iconsole/BluetoothChatService.java | 1 + .../org/surfsite/iconsole/ChannelService.java | 2 + .../iconsole/FEChannelController.java | 316 ++++++++++++++++++ .../res/layout/fragment_bluetooth_chat.xml | 2 +- Application/src/main/res/values/strings.xml | 2 +- android_antlib_4-14/android_antlib_4-14.iml | 26 -- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- iconsole-android.iml | 7 +- 23 files changed, 716 insertions(+), 275 deletions(-) create mode 100644 .idea/caches/build_file_checksums.ser create mode 100644 .idea/caches/gradle_models.ser create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/modules/Application/iconsole-android.Application.iml create mode 100644 .idea/modules/android_antlib_4-14/iconsole-android.android_antlib_4-14.iml delete mode 100644 Application/Application.iml create mode 100644 Application/src/main/java/org/surfsite/iconsole/FEChannelController.java delete mode 100644 android_antlib_4-14/android_antlib_4-14.iml diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser new file mode 100644 index 0000000000000000000000000000000000000000..eceed3041db6c244ea46aea66f8789805afbf3b1 GIT binary patch literal 605 zcmZ4UmVvdnh`~NNKUXg?FQq6yGexf?KR>5fFEb@IQ7^qHF(oHeub?PDD>b=9F91S2 zm1gFoxMk*~I%lLNXBU^|7Q2L-Ts|(GuF1r}V)(xFsf+RnE1rJlAO$>coSVi6a6Gm7{E*_WT<0c1KD)V;NY%53y&Ieij=djS$+C` z2?Gyyg=R;iL$>=UEZD)UHno1&q%S250y+7~i8-L4E=VmZ$xJOSg`0XWcJUe~33*YE zNq_RRD}T)^VNll(Elw>e*3U>RO3X>ohq^-_5nUK&T#@a$+0U?vPi?S p9199^GLsWaGV}9rc&E)KyL{{B*wwv<%B?k;{q0K_1WB->0s!51)tmqT literal 0 HcmV?d00001 diff --git a/.idea/caches/gradle_models.ser b/.idea/caches/gradle_models.ser new file mode 100644 index 0000000000000000000000000000000000000000..91b7e84df749168b38c493ba62727f7cd3c6a7d2 GIT binary patch literal 130606 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 z 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 literal 0 HcmV?d00001 diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..681f41a --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 7c4fca0..8144c3c 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,7 +1,6 @@ - @@ -12,11 +11,6 @@ - - - - - - + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 1c53797..a020387 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,8 +1,11 @@ + diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..bc2751c --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 5bae4e2..65c58fe 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -5,26 +5,47 @@ - + diff --git a/.idea/modules.xml b/.idea/modules.xml index 14a85c1..0188d1c 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,9 +2,9 @@ - - + + \ No newline at end of file diff --git a/.idea/modules/Application/iconsole-android.Application.iml b/.idea/modules/Application/iconsole-android.Application.iml new file mode 100644 index 0000000..9c836b4 --- /dev/null +++ b/.idea/modules/Application/iconsole-android.Application.iml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/android_antlib_4-14/iconsole-android.android_antlib_4-14.iml b/.idea/modules/android_antlib_4-14/iconsole-android.android_antlib_4-14.iml new file mode 100644 index 0000000..41848b4 --- /dev/null +++ b/.idea/modules/android_antlib_4-14/iconsole-android.android_antlib_4-14.iml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Application/Application.iml b/Application/Application.iml deleted file mode 100644 index 153210c..0000000 --- a/Application/Application.iml +++ /dev/null @@ -1,145 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Application/build.gradle b/Application/build.gradle index ebcfc8a..bda7faf 100644 --- a/Application/build.gradle +++ b/Application/build.gradle @@ -10,7 +10,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0' + classpath 'com.android.tools.build:gradle:4.1.3' } } diff --git a/Application/src/main/AndroidManifest.xml b/Application/src/main/AndroidManifest.xml index 2b01c64..33e4dda 100644 --- a/Application/src/main/AndroidManifest.xml +++ b/Application/src/main/AndroidManifest.xml @@ -34,14 +34,16 @@ > - - + + + + diff --git a/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java b/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java index 8087e1f..b7dc91b 100644 --- a/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java +++ b/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java @@ -44,10 +44,104 @@ import android.widget.NumberPicker; import android.widget.TextView; import android.widget.Toast; +import java.lang.ref.WeakReference; + /** * This fragment controls Bluetooth to communicate with other devices. */ public class BluetoothChatFragment extends Fragment { + static class MyInnerHandler extends Handler{ + WeakReference mFrag; + + MyInnerHandler(BluetoothChatFragment aFragment) { + mFrag = new WeakReference(aFragment); + } + + @Override + public void handleMessage(Message msg) { + BluetoothChatFragment theFrag = mFrag.get(); + if (theFrag == null) { + return; + } + FragmentActivity activity = theFrag.getActivity(); + + switch (msg.what) { + case Constants.MESSAGE_STATE_CHANGE: + switch (msg.arg1) { + case BluetoothChatService.STATE_CONNECTED: + theFrag.setStatus(theFrag.getString(R.string.title_connected_to, theFrag.mConnectedDeviceName)); + //mConversationArrayAdapter.clear(); + theFrag.mStartButton.setEnabled(true); + theFrag.mStopButton.setEnabled(true); + theFrag.mDisconnectButton.setEnabled(true); + theFrag.mLevel.setEnabled(true); + theFrag.mLevel.setValue(1); + break; + case BluetoothChatService.STATE_CONNECTING: + theFrag.setStatus(R.string.title_connecting); + theFrag.mStartButton.setEnabled(false); + theFrag.mStopButton.setEnabled(false); + theFrag.mDisconnectButton.setEnabled(false); + theFrag.mLevel.setEnabled(false); + break; + case BluetoothChatService.STATE_LISTEN: + case BluetoothChatService.STATE_NONE: + theFrag.setStatus(R.string.title_not_connected); + theFrag.mStartButton.setEnabled(false); + theFrag.mStopButton.setEnabled(false); + theFrag.mDisconnectButton.setEnabled(false); + theFrag.mLevel.setEnabled(false); + break; + } + break; + case Constants.MESSAGE_DATA: + if (!(msg.obj instanceof IConsole.Data)) + return; + IConsole.Data data = (IConsole.Data) msg.obj; + theFrag.mChannelService.setSpeed(data.mSpeed10 / 10.0); + theFrag.mChannelService.setPower(data.mPower10 * 9 / 100); + theFrag.mChannelService.setCadence(data.mRPM); + + theFrag.mSpeedText.setText(String.format("% 3.1f", data.mSpeed10 / 10.0)); + theFrag.mPowerText.setText(String.format("% 3.1f", data.mPower10 / 10.0)); + theFrag.mRPMText.setText(String.format("%d", data.mRPM)); + theFrag.mDistanceText.setText(String.format("% 3.1f", data.mDistance10 / 10.0)); + theFrag.mCaloriesText.setText(String.format("% 3d", data.mCalories)); + theFrag.mHFText.setText(String.format("%d", data.mHF)); + theFrag.mTimeText.setText(String.format("%s", data.getTimeStr())); + //mLevel.setValue(data.mLevel); + break; + case Constants.MESSAGE_WRITE: + //byte[] writeBuf = (byte[]) msg.obj; + // construct a string from the buffer + //String writeMessage = new String(writeBuf); + //mConversationArrayAdapter.add("Me: " + writeMessage); + break; + case Constants.MESSAGE_READ: + //byte[] readBuf = (byte[]) msg.obj; + // construct a string from the valid bytes in the buffer + //String readMessage = new String(readBuf, 0, msg.arg1); + //mConversationArrayAdapter.add(mConnectedDeviceName + ": " + readMessage); + break; + case Constants.MESSAGE_DEVICE_NAME: + // save the connected device's name + theFrag.mConnectedDeviceName = msg.getData().getString(Constants.DEVICE_NAME); + if (null != activity) { + Toast.makeText(activity, "Connected to " + + theFrag.mConnectedDeviceName, Toast.LENGTH_SHORT).show(); + } + break; + case Constants.MESSAGE_TOAST: + if (null != activity) { + Toast.makeText(activity, msg.getData().getString(Constants.TOAST), + Toast.LENGTH_SHORT).show(); + } + break; + } + } + } + + private final Handler mHandler = new MyInnerHandler(this); private static final String TAG = "BluetoothChatFragment"; @@ -92,86 +186,6 @@ public class BluetoothChatFragment extends Fragment { /** * The Handler that gets information back from the BluetoothChatService */ - private final Handler mHandler = new Handler() { - @SuppressLint("DefaultLocale") - @Override - public void handleMessage(Message msg) { - FragmentActivity activity = getActivity(); - switch (msg.what) { - case Constants.MESSAGE_STATE_CHANGE: - switch (msg.arg1) { - case BluetoothChatService.STATE_CONNECTED: - setStatus(getString(R.string.title_connected_to, mConnectedDeviceName)); - //mConversationArrayAdapter.clear(); - mStartButton.setEnabled(true); - mStopButton.setEnabled(true); - mDisconnectButton.setEnabled(true); - mLevel.setEnabled(true); - mLevel.setValue(1); - break; - case BluetoothChatService.STATE_CONNECTING: - setStatus(R.string.title_connecting); - mStartButton.setEnabled(false); - mStopButton.setEnabled(false); - mDisconnectButton.setEnabled(false); - mLevel.setEnabled(false); - break; - case BluetoothChatService.STATE_LISTEN: - case BluetoothChatService.STATE_NONE: - setStatus(R.string.title_not_connected); - mStartButton.setEnabled(false); - mStopButton.setEnabled(false); - mDisconnectButton.setEnabled(false); - mLevel.setEnabled(false); - break; - } - break; - case Constants.MESSAGE_DATA: - if (!(msg.obj instanceof IConsole.Data)) - return; - IConsole.Data data = (IConsole.Data) msg.obj; - mChannelService.setSpeed(data.mSpeed10 / 10.0); - mChannelService.setPower(data.mPower10 / 10); - mChannelService.setCadence(data.mRPM); - - mSpeedText.setText(String.format("% 3.1f", data.mSpeed10 / 10.0)); - mPowerText.setText(String.format("% 3.1f", data.mPower10 / 10.0)); - mRPMText.setText(String.format("%d", data.mRPM)); - mDistanceText.setText(String.format("% 3.1f", data.mDistance10 / 10.0)); - mCaloriesText.setText(String.format("% 3d", data.mCalories)); - mHFText.setText(String.format("%d", data.mHF)); - mTimeText.setText(String.format("%s", data.getTimeStr())); - //mLevel.setValue(data.mLevel); - break; - case Constants.MESSAGE_WRITE: - //byte[] writeBuf = (byte[]) msg.obj; - // construct a string from the buffer - //String writeMessage = new String(writeBuf); - //mConversationArrayAdapter.add("Me: " + writeMessage); - break; - case Constants.MESSAGE_READ: - //byte[] readBuf = (byte[]) msg.obj; - // construct a string from the valid bytes in the buffer - //String readMessage = new String(readBuf, 0, msg.arg1); - //mConversationArrayAdapter.add(mConnectedDeviceName + ": " + readMessage); - break; - case Constants.MESSAGE_DEVICE_NAME: - // save the connected device's name - mConnectedDeviceName = msg.getData().getString(Constants.DEVICE_NAME); - if (null != activity) { - Toast.makeText(activity, "Connected to " - + mConnectedDeviceName, Toast.LENGTH_SHORT).show(); - } - break; - case Constants.MESSAGE_TOAST: - if (null != activity) { - Toast.makeText(activity, msg.getData().getString(Constants.TOAST), - Toast.LENGTH_SHORT).show(); - } - break; - } - } - }; private boolean mChannelServiceBound = false; private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { diff --git a/Application/src/main/java/org/surfsite/iconsole/BluetoothChatService.java b/Application/src/main/java/org/surfsite/iconsole/BluetoothChatService.java index 1085fc7..0303cd3 100644 --- a/Application/src/main/java/org/surfsite/iconsole/BluetoothChatService.java +++ b/Application/src/main/java/org/surfsite/iconsole/BluetoothChatService.java @@ -141,6 +141,7 @@ public class BluetoothChatService extends Service { mNewState = mState; // Give the new state to the Handler so the UI Activity can update + if (mHandler != null) mHandler.obtainMessage(Constants.MESSAGE_STATE_CHANGE, mNewState, -1).sendToTarget(); } diff --git a/Application/src/main/java/org/surfsite/iconsole/ChannelService.java b/Application/src/main/java/org/surfsite/iconsole/ChannelService.java index 3377fb5..26ba4cc 100644 --- a/Application/src/main/java/org/surfsite/iconsole/ChannelService.java +++ b/Application/src/main/java/org/surfsite/iconsole/ChannelService.java @@ -127,7 +127,9 @@ public class ChannelService extends Service { } private void closeAllChannels() { + if (powerChannelController != null) powerChannelController.close(); + if (speedChannelController != null) speedChannelController.close(); powerChannelController = null; speedChannelController = null; diff --git a/Application/src/main/java/org/surfsite/iconsole/FEChannelController.java b/Application/src/main/java/org/surfsite/iconsole/FEChannelController.java new file mode 100644 index 0000000..a4330dd --- /dev/null +++ b/Application/src/main/java/org/surfsite/iconsole/FEChannelController.java @@ -0,0 +1,316 @@ +/* + * Copyright 2012 Dynastream Innovations Inc. + * + * 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 org.surfsite.iconsole; + +import android.os.RemoteException; +import android.util.Log; + +import com.dsi.ant.channel.AntChannel; +import com.dsi.ant.channel.AntCommandFailedException; +import com.dsi.ant.channel.Capabilities; +import com.dsi.ant.channel.IAntChannelEventHandler; +import com.dsi.ant.message.ChannelId; +import com.dsi.ant.message.ChannelType; +import com.dsi.ant.message.EventCode; +import com.dsi.ant.message.fromant.AcknowledgedDataMessage; +import com.dsi.ant.message.fromant.ChannelEventMessage; +import com.dsi.ant.message.fromant.MessageFromAntType; +import com.dsi.ant.message.ipc.AntMessageParcel; + +import java.util.Random; + +public class FEChannelController { + public static final int FE_SENSOR_ID = 0x9e3d4b67; + // The device type and transmission type to be part of the channel ID message + private static final int CHANNEL_FE_DEVICE_TYPE = 0x11; + private static final int CHANNEL_FE_TRANSMISSION_TYPE = 5; + // The period and frequency values the channel will be configured to + private static final int CHANNEL_POWER_PERIOD = 8182; // 1 Hz + private static final int CHANNEL_POWER_FREQUENCY = 57; + private static final String TAG = FEChannelController.class.getSimpleName(); + private static Random randGen = new Random(); + int power = 0; + int cadence = 0; + private AntChannel mAntChannel; + private ChannelEventCallback mChannelEventCallback = new ChannelEventCallback(); + private boolean mIsOpen; + + public FEChannelController(AntChannel antChannel) { + mAntChannel = antChannel; + openChannel(); + } + + boolean openChannel() { + if (null != mAntChannel) { + if (mIsOpen) { + Log.w(TAG, "Channel was already open"); + } else { + // Channel ID message contains device number, type and transmission type. In + // order for master (TX) channels and slave (RX) channels to connect, they + // must have the same channel ID, or wildcard (0) is used. + ChannelId channelId = new ChannelId(FE_SENSOR_ID & 0xFFFF, + CHANNEL_FE_DEVICE_TYPE, CHANNEL_FE_TRANSMISSION_TYPE); + + try { + // Setting the channel event handler so that we can receive messages from ANT + mAntChannel.setChannelEventHandler(mChannelEventCallback); + + // Performs channel assignment by assigning the type to the channel. Additional + // features (such as, background scanning and frequency agility) can be enabled + // by passing an ExtendedAssignment object to assign(ChannelType, ExtendedAssignment). + mAntChannel.assign(ChannelType.BIDIRECTIONAL_MASTER); + + /* + * Configures the channel ID, messaging period and rf frequency after assigning, + * then opening the channel. + * + * For any additional ANT features such as proximity search or background scanning, refer to + * the ANT Protocol Doc found at: + * http://www.thisisant.com/resources/ant-message-protocol-and-usage/ + */ + mAntChannel.setChannelId(channelId); + mAntChannel.setPeriod(CHANNEL_POWER_PERIOD); + mAntChannel.setRfFrequency(CHANNEL_POWER_FREQUENCY); + mAntChannel.open(); + mIsOpen = true; + + Log.d(TAG, "Opened channel with device number: " + FE_SENSOR_ID); + } catch (RemoteException e) { + channelError(e); + } catch (AntCommandFailedException e) { + // This will release, and therefore unassign if required + channelError("Open failed", e); + } + } + } else { + Log.w(TAG, "No channel available"); + } + + return mIsOpen; + } + + + void channelError(RemoteException e) { + String logString = "Remote service communication failed."; + + Log.e(TAG, logString); + + } + + void channelError(String error, AntCommandFailedException e) { + StringBuilder logString; + + if (e.getResponseMessage() != null) { + String initiatingMessageId = "0x" + Integer.toHexString( + e.getResponseMessage().getInitiatingMessageId()); + String rawResponseCode = "0x" + Integer.toHexString( + e.getResponseMessage().getRawResponseCode()); + + logString = new StringBuilder(error) + .append(". Command ") + .append(initiatingMessageId) + .append(" failed with code ") + .append(rawResponseCode); + } else { + String attemptedMessageId = "0x" + Integer.toHexString( + e.getAttemptedMessageType().getMessageId()); + String failureReason = e.getFailureReason().toString(); + + logString = new StringBuilder(error) + .append(". Command ") + .append(attemptedMessageId) + .append(" failed with reason ") + .append(failureReason); + } + + Log.e(TAG, logString.toString()); + + mAntChannel.release(); + } + + public void close() { + // TODO kill all our resources + if (null != mAntChannel) { + mIsOpen = false; + + // Releasing the channel to make it available for others. + // After releasing, the AntChannel instance cannot be reused. + mAntChannel.release(); + mAntChannel = null; + } + + Log.e(TAG, "Channel Closed"); + } + + /** + * Implements the Channel Event Handler Interface so that messages can be + * received and channel death events can be handled. + */ + public class ChannelEventCallback implements IAntChannelEventHandler { + + int cnt = 0; + int eventCount = 0; + int cumulativePower = 0; + + @Override + public void onChannelDeath() { + // Display channel death message when channel dies + Log.e(TAG, "Channel Death"); + } + + @Override + public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) { + Log.d(TAG, "Rx: " + antParcel); + Log.d(TAG, "Message Type: " + messageType); + byte[] payload = new byte[8]; + + byte fe_state = 0; // RESERVED + fe_state = 1; // ASLEEP + fe_state = 2; // READY + fe_state = 3; // IN_USE + fe_state = 4; // FINISHED / PAUSE + + // Switching on message type to handle different types of messages + switch (messageType) { + // If data message, construct from parcel and update channel data + case BROADCAST_DATA: + // Rx Data + //updateData(new BroadcastDataMessage(antParcel).getPayload()); + break; + case ACKNOWLEDGED_DATA: + // Rx Data + //updateData(new AcknowledgedDataMessage(antParcel).getPayload()); + payload = new AcknowledgedDataMessage(antParcel).getPayload(); + Log.d(TAG, "AcknowledgedDataMessage: " + payload); + + if ((payload[0] == 0) && (payload[1] == 1) && (payload[2] == (byte)0xAA)) { + payload[0] = (byte) 0x01; + payload[1] = (byte) 0xAC; + payload[2] = (byte) 0xFF; + payload[3] = (byte) 0xFF; + payload[4] = (byte) 0xFF; + payload[5] = (byte) 0xFF; + payload[6] = (byte) 0x00; + payload[7] = (byte) 0x00; + try { + // Setting the data to be broadcast on the next channel period + mAntChannel.setBroadcastData(payload); + } catch (RemoteException e) { + channelError(e); + } + } + break; + case CHANNEL_EVENT: + // Constructing channel event message from parcel + ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel); + EventCode code = eventMessage.getEventCode(); + Log.d(TAG, "Event Code: " + code); + + // Switching on event code to handle the different types of channel events + switch (code) { + case TX: + cnt += 1; + + if (cnt % 66 == 64) { + payload[0] = (byte) 0x50; + payload[1] = (byte) 0xFF; + payload[2] = (byte) 0xFF; + payload[3] = (byte) 0x01; + payload[4] = (byte) 0xFF; + payload[5] = (byte) 0x00; + payload[6] = (byte) 0x01; + payload[7] = (byte) 0x00; + } else if (cnt % 66 == 65) { + payload[0] = (byte) 0x51; + payload[1] = (byte) 0xFF; + payload[2] = (byte) 0xFF; + payload[3] = (byte) 0x01; + payload[4] = (byte) ((FE_SENSOR_ID) & 0xFF); + payload[5] = (byte) ((FE_SENSOR_ID >> 8) & 0xFF); + payload[6] = (byte) ((FE_SENSOR_ID >> 16) & 0xFF); + payload[7] = (byte) ((FE_SENSOR_ID >> 24) & 0xFF); + } else if (cnt % 2 == 0) { + // PAGE 16 + payload[0] = (byte) 0x10; + payload[1] = (byte) 25; // Equipment Type: 25 == Trainer / Stationary Bike + payload[2] = (byte) elapsedTime_in_0_25_seconds; + payload[3] = (byte) 0; // Distance traveled + payload[4] = (byte) 0xFF; // Speed LSB + payload[5] = (byte) 0xFF; // Speed MSB + payload[6] = (byte) 0xFF; // heart rate + payload[7] = (byte) (fe_state >> 4); // Capabilities 0:3 ; FE State Bit Field 4:7 + } else { + // PAGE 25 + eventCount = (eventCount + 1) & 0xFF; + cumulativePower = (cumulativePower + power) & 0xFFFF; + byte flags_bit_field; + flags_bit_field = 1; // too slow to achieve target power + flags_bit_field = 2; // too fast to achieve target power + flags_bit_field = 0; // operating at target power + + payload[0] = (byte) 0x19; + payload[1] = (byte) eventCount; + payload[2] = (byte) cadence; + payload[3] = (byte) ((cumulativePower) & 0xFF); + payload[4] = (byte) ((cumulativePower >> 8) & 0xFF); + payload[5] = (byte) ((power) & 0xFF); + payload[6] = (byte) (((power >> 8) & 0xFF) | (trainerStatusBits >> 4)); + payload[7] = (byte) ((flags_bit_field & 0xF) | ((fe_state >> 4) & 0xF0)); // Capabilities 0:3 ; FE State Bit Field 4:7 + } + } + + if (mIsOpen) { + try { + // Setting the data to be broadcast on the next channel period + mAntChannel.setBroadcastData(payload); + } catch (RemoteException e) { + channelError(e); + } + } + break; + case CHANNEL_COLLISION: + cnt += 1; + break; + case RX_SEARCH_TIMEOUT: + // TODO May want to keep searching + Log.e(TAG, "No Device Found"); + break; + case CHANNEL_CLOSED: + case RX_FAIL: + case RX_FAIL_GO_TO_SEARCH: + case TRANSFER_RX_FAILED: + case TRANSFER_TX_COMPLETED: + case TRANSFER_TX_FAILED: + case TRANSFER_TX_START: + case UNKNOWN: + // TODO More complex communication will need to handle these events + break; + } + break; + case ANT_VERSION: + case BURST_TRANSFER_DATA: + case CAPABILITIES: + case CHANNEL_ID: + case CHANNEL_RESPONSE: + case CHANNEL_STATUS: + case SERIAL_NUMBER: + case OTHER: + // TODO More complex communication will need to handle these message types + break; + } + } + } +} diff --git a/Application/src/main/res/layout/fragment_bluetooth_chat.xml b/Application/src/main/res/layout/fragment_bluetooth_chat.xml index 95338db..f47b525 100644 --- a/Application/src/main/res/layout/fragment_bluetooth_chat.xml +++ b/Application/src/main/res/layout/fragment_bluetooth_chat.xml @@ -101,7 +101,7 @@ android:layout_marginLeft="16dp" android:layout_marginRight="16dp" android:layout_marginTop="16dp" - android:text="Power" + android:text="Power corr" app:layout_constraintBottom_toTopOf="@+id/Power" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toLeftOf="@+id/guidelineleft" diff --git a/Application/src/main/res/values/strings.xml b/Application/src/main/res/values/strings.xml index 4b9e87c..8b6a118 100644 --- a/Application/src/main/res/values/strings.xml +++ b/Application/src/main/res/values/strings.xml @@ -46,7 +46,7 @@ 0.0 Level OpeniConsole Bluetooth active - OpeniConsole + OpeniConsole - TEST Connect to iConsole diff --git a/android_antlib_4-14/android_antlib_4-14.iml b/android_antlib_4-14/android_antlib_4-14.iml deleted file mode 100644 index 47b5059..0000000 --- a/android_antlib_4-14/android_antlib_4-14.iml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7487467..cf780a6 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0' + classpath 'com.android.tools.build:gradle:4.1.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 17ebf47..e4259d6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun May 12 14:24:25 CEST 2019 +#Mon Apr 12 12:52:47 CEST 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip diff --git a/iconsole-android.iml b/iconsole-android.iml index 24c2e1d..74be301 100644 --- a/iconsole-android.iml +++ b/iconsole-android.iml @@ -1,5 +1,5 @@ - + @@ -8,11 +8,12 @@ - + + - + \ No newline at end of file From 449c7e34f46eda35c83b4d748692c5df40463622 Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Wed, 26 May 2021 16:33:10 +0200 Subject: [PATCH 8/9] undo me --- .../iconsole/FEChannelController.java | 316 ------------------ 1 file changed, 316 deletions(-) delete mode 100644 Application/src/main/java/org/surfsite/iconsole/FEChannelController.java diff --git a/Application/src/main/java/org/surfsite/iconsole/FEChannelController.java b/Application/src/main/java/org/surfsite/iconsole/FEChannelController.java deleted file mode 100644 index a4330dd..0000000 --- a/Application/src/main/java/org/surfsite/iconsole/FEChannelController.java +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Copyright 2012 Dynastream Innovations Inc. - * - * 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 org.surfsite.iconsole; - -import android.os.RemoteException; -import android.util.Log; - -import com.dsi.ant.channel.AntChannel; -import com.dsi.ant.channel.AntCommandFailedException; -import com.dsi.ant.channel.Capabilities; -import com.dsi.ant.channel.IAntChannelEventHandler; -import com.dsi.ant.message.ChannelId; -import com.dsi.ant.message.ChannelType; -import com.dsi.ant.message.EventCode; -import com.dsi.ant.message.fromant.AcknowledgedDataMessage; -import com.dsi.ant.message.fromant.ChannelEventMessage; -import com.dsi.ant.message.fromant.MessageFromAntType; -import com.dsi.ant.message.ipc.AntMessageParcel; - -import java.util.Random; - -public class FEChannelController { - public static final int FE_SENSOR_ID = 0x9e3d4b67; - // The device type and transmission type to be part of the channel ID message - private static final int CHANNEL_FE_DEVICE_TYPE = 0x11; - private static final int CHANNEL_FE_TRANSMISSION_TYPE = 5; - // The period and frequency values the channel will be configured to - private static final int CHANNEL_POWER_PERIOD = 8182; // 1 Hz - private static final int CHANNEL_POWER_FREQUENCY = 57; - private static final String TAG = FEChannelController.class.getSimpleName(); - private static Random randGen = new Random(); - int power = 0; - int cadence = 0; - private AntChannel mAntChannel; - private ChannelEventCallback mChannelEventCallback = new ChannelEventCallback(); - private boolean mIsOpen; - - public FEChannelController(AntChannel antChannel) { - mAntChannel = antChannel; - openChannel(); - } - - boolean openChannel() { - if (null != mAntChannel) { - if (mIsOpen) { - Log.w(TAG, "Channel was already open"); - } else { - // Channel ID message contains device number, type and transmission type. In - // order for master (TX) channels and slave (RX) channels to connect, they - // must have the same channel ID, or wildcard (0) is used. - ChannelId channelId = new ChannelId(FE_SENSOR_ID & 0xFFFF, - CHANNEL_FE_DEVICE_TYPE, CHANNEL_FE_TRANSMISSION_TYPE); - - try { - // Setting the channel event handler so that we can receive messages from ANT - mAntChannel.setChannelEventHandler(mChannelEventCallback); - - // Performs channel assignment by assigning the type to the channel. Additional - // features (such as, background scanning and frequency agility) can be enabled - // by passing an ExtendedAssignment object to assign(ChannelType, ExtendedAssignment). - mAntChannel.assign(ChannelType.BIDIRECTIONAL_MASTER); - - /* - * Configures the channel ID, messaging period and rf frequency after assigning, - * then opening the channel. - * - * For any additional ANT features such as proximity search or background scanning, refer to - * the ANT Protocol Doc found at: - * http://www.thisisant.com/resources/ant-message-protocol-and-usage/ - */ - mAntChannel.setChannelId(channelId); - mAntChannel.setPeriod(CHANNEL_POWER_PERIOD); - mAntChannel.setRfFrequency(CHANNEL_POWER_FREQUENCY); - mAntChannel.open(); - mIsOpen = true; - - Log.d(TAG, "Opened channel with device number: " + FE_SENSOR_ID); - } catch (RemoteException e) { - channelError(e); - } catch (AntCommandFailedException e) { - // This will release, and therefore unassign if required - channelError("Open failed", e); - } - } - } else { - Log.w(TAG, "No channel available"); - } - - return mIsOpen; - } - - - void channelError(RemoteException e) { - String logString = "Remote service communication failed."; - - Log.e(TAG, logString); - - } - - void channelError(String error, AntCommandFailedException e) { - StringBuilder logString; - - if (e.getResponseMessage() != null) { - String initiatingMessageId = "0x" + Integer.toHexString( - e.getResponseMessage().getInitiatingMessageId()); - String rawResponseCode = "0x" + Integer.toHexString( - e.getResponseMessage().getRawResponseCode()); - - logString = new StringBuilder(error) - .append(". Command ") - .append(initiatingMessageId) - .append(" failed with code ") - .append(rawResponseCode); - } else { - String attemptedMessageId = "0x" + Integer.toHexString( - e.getAttemptedMessageType().getMessageId()); - String failureReason = e.getFailureReason().toString(); - - logString = new StringBuilder(error) - .append(". Command ") - .append(attemptedMessageId) - .append(" failed with reason ") - .append(failureReason); - } - - Log.e(TAG, logString.toString()); - - mAntChannel.release(); - } - - public void close() { - // TODO kill all our resources - if (null != mAntChannel) { - mIsOpen = false; - - // Releasing the channel to make it available for others. - // After releasing, the AntChannel instance cannot be reused. - mAntChannel.release(); - mAntChannel = null; - } - - Log.e(TAG, "Channel Closed"); - } - - /** - * Implements the Channel Event Handler Interface so that messages can be - * received and channel death events can be handled. - */ - public class ChannelEventCallback implements IAntChannelEventHandler { - - int cnt = 0; - int eventCount = 0; - int cumulativePower = 0; - - @Override - public void onChannelDeath() { - // Display channel death message when channel dies - Log.e(TAG, "Channel Death"); - } - - @Override - public void onReceiveMessage(MessageFromAntType messageType, AntMessageParcel antParcel) { - Log.d(TAG, "Rx: " + antParcel); - Log.d(TAG, "Message Type: " + messageType); - byte[] payload = new byte[8]; - - byte fe_state = 0; // RESERVED - fe_state = 1; // ASLEEP - fe_state = 2; // READY - fe_state = 3; // IN_USE - fe_state = 4; // FINISHED / PAUSE - - // Switching on message type to handle different types of messages - switch (messageType) { - // If data message, construct from parcel and update channel data - case BROADCAST_DATA: - // Rx Data - //updateData(new BroadcastDataMessage(antParcel).getPayload()); - break; - case ACKNOWLEDGED_DATA: - // Rx Data - //updateData(new AcknowledgedDataMessage(antParcel).getPayload()); - payload = new AcknowledgedDataMessage(antParcel).getPayload(); - Log.d(TAG, "AcknowledgedDataMessage: " + payload); - - if ((payload[0] == 0) && (payload[1] == 1) && (payload[2] == (byte)0xAA)) { - payload[0] = (byte) 0x01; - payload[1] = (byte) 0xAC; - payload[2] = (byte) 0xFF; - payload[3] = (byte) 0xFF; - payload[4] = (byte) 0xFF; - payload[5] = (byte) 0xFF; - payload[6] = (byte) 0x00; - payload[7] = (byte) 0x00; - try { - // Setting the data to be broadcast on the next channel period - mAntChannel.setBroadcastData(payload); - } catch (RemoteException e) { - channelError(e); - } - } - break; - case CHANNEL_EVENT: - // Constructing channel event message from parcel - ChannelEventMessage eventMessage = new ChannelEventMessage(antParcel); - EventCode code = eventMessage.getEventCode(); - Log.d(TAG, "Event Code: " + code); - - // Switching on event code to handle the different types of channel events - switch (code) { - case TX: - cnt += 1; - - if (cnt % 66 == 64) { - payload[0] = (byte) 0x50; - payload[1] = (byte) 0xFF; - payload[2] = (byte) 0xFF; - payload[3] = (byte) 0x01; - payload[4] = (byte) 0xFF; - payload[5] = (byte) 0x00; - payload[6] = (byte) 0x01; - payload[7] = (byte) 0x00; - } else if (cnt % 66 == 65) { - payload[0] = (byte) 0x51; - payload[1] = (byte) 0xFF; - payload[2] = (byte) 0xFF; - payload[3] = (byte) 0x01; - payload[4] = (byte) ((FE_SENSOR_ID) & 0xFF); - payload[5] = (byte) ((FE_SENSOR_ID >> 8) & 0xFF); - payload[6] = (byte) ((FE_SENSOR_ID >> 16) & 0xFF); - payload[7] = (byte) ((FE_SENSOR_ID >> 24) & 0xFF); - } else if (cnt % 2 == 0) { - // PAGE 16 - payload[0] = (byte) 0x10; - payload[1] = (byte) 25; // Equipment Type: 25 == Trainer / Stationary Bike - payload[2] = (byte) elapsedTime_in_0_25_seconds; - payload[3] = (byte) 0; // Distance traveled - payload[4] = (byte) 0xFF; // Speed LSB - payload[5] = (byte) 0xFF; // Speed MSB - payload[6] = (byte) 0xFF; // heart rate - payload[7] = (byte) (fe_state >> 4); // Capabilities 0:3 ; FE State Bit Field 4:7 - } else { - // PAGE 25 - eventCount = (eventCount + 1) & 0xFF; - cumulativePower = (cumulativePower + power) & 0xFFFF; - byte flags_bit_field; - flags_bit_field = 1; // too slow to achieve target power - flags_bit_field = 2; // too fast to achieve target power - flags_bit_field = 0; // operating at target power - - payload[0] = (byte) 0x19; - payload[1] = (byte) eventCount; - payload[2] = (byte) cadence; - payload[3] = (byte) ((cumulativePower) & 0xFF); - payload[4] = (byte) ((cumulativePower >> 8) & 0xFF); - payload[5] = (byte) ((power) & 0xFF); - payload[6] = (byte) (((power >> 8) & 0xFF) | (trainerStatusBits >> 4)); - payload[7] = (byte) ((flags_bit_field & 0xF) | ((fe_state >> 4) & 0xF0)); // Capabilities 0:3 ; FE State Bit Field 4:7 - } - } - - if (mIsOpen) { - try { - // Setting the data to be broadcast on the next channel period - mAntChannel.setBroadcastData(payload); - } catch (RemoteException e) { - channelError(e); - } - } - break; - case CHANNEL_COLLISION: - cnt += 1; - break; - case RX_SEARCH_TIMEOUT: - // TODO May want to keep searching - Log.e(TAG, "No Device Found"); - break; - case CHANNEL_CLOSED: - case RX_FAIL: - case RX_FAIL_GO_TO_SEARCH: - case TRANSFER_RX_FAILED: - case TRANSFER_TX_COMPLETED: - case TRANSFER_TX_FAILED: - case TRANSFER_TX_START: - case UNKNOWN: - // TODO More complex communication will need to handle these events - break; - } - break; - case ANT_VERSION: - case BURST_TRANSFER_DATA: - case CAPABILITIES: - case CHANNEL_ID: - case CHANNEL_RESPONSE: - case CHANNEL_STATUS: - case SERIAL_NUMBER: - case OTHER: - // TODO More complex communication will need to handle these message types - break; - } - } - } -} From d2ab7de012204cacf622e13824b9291573ae2ded Mon Sep 17 00:00:00 2001 From: Harald Hoyer Date: Thu, 24 Jun 2021 09:23:03 +0200 Subject: [PATCH 9/9] update --- .idea/caches/build_file_checksums.ser | Bin 605 -> 605 bytes .../iconsole-android.Application.iml | 4 ++-- Application/build.gradle | 2 +- .../iconsole/BluetoothChatFragment.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index eceed3041db6c244ea46aea66f8789805afbf3b1..23e7dfb2895a06eb7557cf07856ff36ef55b4a25 100644 GIT binary patch delta 37 tcmcc1a+hVobk@q1a<$hs&S_#4xcD<8&sH))sO>~(x_-d<3zPMj^Z`6f599y< delta 37 tcmcc1a+hVobk>Ta+cukRoYTZ8&}NfezIAi#>fS@;)*8+J_LKFP^Z`Yy59 - + + @@ -77,7 +78,6 @@ - \ No newline at end of file diff --git a/Application/build.gradle b/Application/build.gradle index bda7faf..dd745bc 100644 --- a/Application/build.gradle +++ b/Application/build.gradle @@ -29,7 +29,7 @@ dependencies { implementation "com.android.support:cardview-v7:28.0.0" implementation "com.android.support:appcompat-v7:28.0.0" implementation 'com.android.support:support-v13:28.0.0' - implementation 'com.android.support.constraint:constraint-layout:1.1.3' + implementation 'com.android.support.constraint:constraint-layout:2.0.4' implementation project(':android_antlib_4-14') } diff --git a/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java b/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java index b7dc91b..fa43b09 100644 --- a/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java +++ b/Application/src/main/java/org/surfsite/iconsole/BluetoothChatFragment.java @@ -99,7 +99,7 @@ public class BluetoothChatFragment extends Fragment { return; IConsole.Data data = (IConsole.Data) msg.obj; theFrag.mChannelService.setSpeed(data.mSpeed10 / 10.0); - theFrag.mChannelService.setPower(data.mPower10 * 9 / 100); + theFrag.mChannelService.setPower(data.mPower10 / 10); theFrag.mChannelService.setCadence(data.mRPM); theFrag.mSpeedText.setText(String.format("% 3.1f", data.mSpeed10 / 10.0));