From 1aad832606b8630d9de47cb74bb9ba376f3df038 Mon Sep 17 00:00:00 2001 From: William Skaggs Date: Sun, 7 Oct 2018 13:04:23 -0500 Subject: [PATCH 1/2] Update sources, assets, res, and manifest --- build.gradle | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- library/build.gradle | 5 +- library/src/main/AndroidManifest.xml | 19 +- .../src/main/assets/RobotConfigTaxonomy.xml | 12 + .../com/qualcomm/ftccommon/AboutActivity.java | 248 ---- .../ftccommon/ClassManagerFactory.java | 4 +- .../com/qualcomm/ftccommon/CommandList.java | 107 +- .../qualcomm/ftccommon/FtcAboutActivity.java | 305 ++++ .../FtcAdvancedRCSettingsActivity.java | 4 +- .../com/qualcomm/ftccommon/FtcEventLoop.java | 86 +- .../qualcomm/ftccommon/FtcEventLoopBase.java | 395 ++++-- .../ftccommon/FtcEventLoopHandler.java | 169 ++- .../FtcLynxFirmwareUpdateActivity.java | 122 +- .../FtcLynxModuleAddressUpdateActivity.java | 17 +- .../ftccommon/FtcRobotControllerService.java | 73 +- .../FtcRobotControllerSettingsActivity.java | 14 +- .../FtcWifiDirectChannelSelectorActivity.java | 38 +- .../LaunchActivityConstantsList.java | 1 - .../com/qualcomm/ftccommon/SoundPlayer.java | 1223 ++++++++++++++--- .../ftccommon/USBAccessibleLynxModule.java | 27 + .../java/com/qualcomm/ftccommon/UpdateUI.java | 14 +- .../ConfigurationTypeArrayAdapter.java | 68 + .../ConfigureFromTemplateActivity.java | 1 + .../ftccommon/configuration/EditActivity.java | 56 +- .../EditAnalogInputDevicesActivity.java | 7 + .../EditAnalogOutputDevicesActivity.java | 8 + .../EditDeviceInterfaceModuleActivity.java | 36 +- .../EditDigitalDevicesActivity.java | 7 + .../EditI2cDevicesActivityAbstract.java | 36 +- .../EditI2cDevicesActivityLynx.java | 27 + .../EditLegacyModuleControllerActivity.java | 38 +- .../configuration/EditLynxModuleActivity.java | 82 +- .../EditMatrixControllerActivity.java | 6 +- .../configuration/EditMotorListActivity.java | 51 +- .../configuration/EditParameters.java | 55 +- .../configuration/EditPortListActivity.java | 3 +- .../EditPortListSpinnerActivity.java | 27 +- .../configuration/EditServoListActivity.java | 8 + .../configuration/EditUSBDeviceActivity.java | 5 +- .../configuration/EditWebcamActivity.java | 137 ++ .../FtcConfigurationActivity.java | 161 +-- .../ftccommon/configuration/RequestCode.java | 3 +- .../configuration/RobotConfigFileManager.java | 12 +- .../configuration/RobotConfigMap.java | 16 +- .../configuration/ScannedDevices.java | 95 -- .../configuration/USBScanManager.java | 34 +- .../external/SoundPlayingRobotMonitor.java | 91 +- .../internal/ProgramAndManageActivity.java | 21 +- .../src/main/res/layout/activity_about.xml | 13 - .../layout/activity_ftc_lynx_fw_update.xml | 18 +- .../main/res/layout/analog_input_device.xml | 1 - .../main/res/layout/analog_output_device.xml | 1 - .../res/layout/device_interface_module.xml | 2 +- .../src/main/res/layout/digital_device.xml | 1 - .../main/res/layout/digital_device_lynx.xml | 1 - library/src/main/res/layout/i2cs.xml | 2 +- library/src/main/res/layout/legacy.xml | 3 +- library/src/main/res/layout/lynx_module.xml | 2 +- .../src/main/res/layout/lynx_usb_device.xml | 2 +- library/src/main/res/layout/matrices.xml | 3 +- .../res/layout/motor_controller_banner.xml | 3 +- library/src/main/res/layout/servo.xml | 1 - .../res/layout/servo_controller_banner.xml | 3 +- library/src/main/res/layout/webcam_device.xml | 53 + library/src/main/res/values/values.xml | 56 +- .../src/main/res/xml/ftc_about_activity.xml | 81 ++ .../main/res/xml/ftc_about_activity_rc.xml | 80 ++ 68 files changed, 3126 insertions(+), 1180 deletions(-) delete mode 100644 library/src/main/java/com/qualcomm/ftccommon/AboutActivity.java create mode 100644 library/src/main/java/com/qualcomm/ftccommon/FtcAboutActivity.java create mode 100644 library/src/main/java/com/qualcomm/ftccommon/configuration/ConfigurationTypeArrayAdapter.java create mode 100644 library/src/main/java/com/qualcomm/ftccommon/configuration/EditWebcamActivity.java delete mode 100644 library/src/main/java/com/qualcomm/ftccommon/configuration/ScannedDevices.java delete mode 100644 library/src/main/res/layout/activity_about.xml create mode 100644 library/src/main/res/layout/webcam_device.xml create mode 100644 library/src/main/res/xml/ftc_about_activity.xml create mode 100644 library/src/main/res/xml/ftc_about_activity_rc.xml diff --git a/build.gradle b/build.gradle index f16a09f..d04a532 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.ftc_version = '3.7' + ext.ftc_version = '4.0' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.4' + classpath 'com.android.tools.build:gradle:3.2.0' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' // 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 a6a4169..ed34491 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip diff --git a/library/build.gradle b/library/build.gradle index 6b93416..36bc5e8 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -8,7 +8,7 @@ android { targetSdkVersion 19 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - versionCode 43 + versionCode 44 versionName "$ftc_version.0" ndk { @@ -17,6 +17,9 @@ android { consumerProguardFiles 'proguard-rules.pro' } + lintOptions { + abortOnError false + } } group = 'com.github.modular-ftc' diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index d747e72..c27c068 100644 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -1,8 +1,15 @@ + package="com.qualcomm.ftccommon" + android:versionCode="46" + android:versionName="8.0" > + + + @@ -80,6 +87,10 @@ android:name="com.qualcomm.ftccommon.AboutActivity" android:label="@string/about_activity" > + + + + aobus analog output port + + RevRoboticsCoreHexMotor + a REV Robotics Core Hex Motor + nobus + port + + + LynxEmbeddedIMU + the internal IMU + i2cbus + port + diff --git a/library/src/main/java/com/qualcomm/ftccommon/AboutActivity.java b/library/src/main/java/com/qualcomm/ftccommon/AboutActivity.java deleted file mode 100644 index 9c2d35a..0000000 --- a/library/src/main/java/com/qualcomm/ftccommon/AboutActivity.java +++ /dev/null @@ -1,248 +0,0 @@ -/* Copyright (c) 2014. 2015 Qualcomm Technologies Inc - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted (subject to the limitations in the disclaimer below) provided that -the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -Neither the name of Qualcomm Technologies Inc nor the names of its contributors -may be used to endorse or promote products derived from this software without -specific prior written permission. - -NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS -LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - -/* Copyright (c) 2015 Jonathan Berling - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted (subject to the limitations in the disclaimer below) provided that -the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -Neither the name of Jonathan Berling nor the names of its contributors -may be used to endorse or promote products derived from this software without -specific prior written permission. - -NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS -LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ - -package com.qualcomm.ftccommon; - -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ListView; -import android.widget.TextView; - -import com.qualcomm.robotcore.robocol.RobocolConfig; -import com.qualcomm.robotcore.util.Network; -import com.qualcomm.robotcore.util.RobotLog; -import com.qualcomm.robotcore.util.Version; -import com.qualcomm.robotcore.wifi.NetworkConnection; -import com.qualcomm.robotcore.wifi.NetworkConnectionFactory; -import com.qualcomm.robotcore.wifi.NetworkType; - -import org.firstinspires.ftc.robotcore.internal.ui.ThemedActivity; - -import java.io.IOException; -import java.io.Serializable; -import java.net.InetAddress; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -public class AboutActivity extends ThemedActivity { - - @Override public String getTag() { return this.getClass().getSimpleName(); } - - NetworkConnection networkConnection = null; - NetworkType networkType; - - @Override - protected void onStart() { - super.onStart(); - setContentView(R.layout.activity_about); - - Intent intent = getIntent(); - Serializable extra = intent.getSerializableExtra(LaunchActivityConstantsList.ABOUT_ACTIVITY_CONNECTION_TYPE); - if(extra != null) { - networkType = (NetworkType) extra; - } - - - ListView aboutList = (ListView)findViewById(R.id.aboutList); - - try { - networkConnection = NetworkConnectionFactory.getNetworkConnection(networkType, null); - networkConnection.enable(); - } catch (NullPointerException e) { - RobotLog.e("Cannot start Network Connection of type: " + networkType); - networkConnection = null; - } - - ArrayAdapter adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_2, android.R.id.text1) { - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = super.getView(position, convertView, parent); - TextView topLine = (TextView) view.findViewById(android.R.id.text1); - TextView bottomLine = (TextView) view.findViewById(android.R.id.text2); - - Item item = getItem(position); - topLine.setText(item.title); - bottomLine.setText(item.info); - - return view; - } - - @Override - public int getCount() - { - return 5; - } - - @Override - public Item getItem(int pos) { - switch (pos) { - case 0: return getAppVersion(); - case 1: return getLibVersion(); - case 2: return getNetworkProtocolVersion(); - case 3: return getConnectionInfo(); - case 4: return getBuildTimeInfo(); - } - return new Item(); - } - - private Item getAppVersion() { - Item i = new Item(); - i.title = getString(R.string.about_app_version); - try { - i.info = AboutActivity.this.getPackageManager().getPackageInfo(AboutActivity.this.getPackageName(), 0).versionName; - } catch (android.content.pm.PackageManager.NameNotFoundException e) { - i.info = e.getMessage(); - } - return i; - } - - private Item getLibVersion() { - Item i = new Item(); - i.title = getString(R.string.about_library_version); - i.info = Version.getLibraryVersion(); - return i; - } - - private Item getNetworkProtocolVersion() { - Item i = new Item(); - i.title = getString(R.string.about_network_protocol_version); - i.info = String.format("v%d", RobocolConfig.ROBOCOL_VERSION); - return i; - } - - private Item getConnectionInfo() { - Item i = new Item(); - i.title = getString(R.string.about_network_connection_info); - i.info = networkConnection.getInfo(); - - return i; - } - - private Item getBuildTimeInfo() { - Item i = new Item(); - i.title = getString(R.string.about_build_time); - i.info = getBuildTime(); - return i; - } - }; - - aboutList.setAdapter(adapter); - } - - protected void onStop() { - super.onStop(); - - if (networkConnection != null) { - networkConnection.disable(); - } - } - - protected String getLocalIpAddressesAsString() { - ArrayList addrs; - - addrs = Network.getLocalIpAddresses(); - addrs = Network.removeLoopbackAddresses(addrs); - addrs = Network.removeIPv6Addresses(addrs); - - if (addrs.size() < 1) return "unavailable"; - - StringBuilder sb = new StringBuilder(); - sb.append(addrs.get(0).getHostAddress()); - for (int i = 1; i < addrs.size(); i++) { - sb.append(", ").append(addrs.get(i).getHostAddress()); - } - return sb.toString(); - } - - /** https://code.google.com/p/android/issues/detail?id=220039 */ - protected String getBuildTime() { - - String buildTime = "unavailable"; - try { - ApplicationInfo ai = getPackageManager().getApplicationInfo(getPackageName(), 0); - - ZipFile zf = new ZipFile(ai.sourceDir); - ZipEntry ze = zf.getEntry("classes.dex"); - zf.close(); - - long time = ze.getTime(); - buildTime = SimpleDateFormat.getInstance().format(new java.util.Date(time)); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - return buildTime; - } - - public static class Item { - public String title = ""; - public String info = ""; - } -} - diff --git a/library/src/main/java/com/qualcomm/ftccommon/ClassManagerFactory.java b/library/src/main/java/com/qualcomm/ftccommon/ClassManagerFactory.java index f431841..36f70cb 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/ClassManagerFactory.java +++ b/library/src/main/java/com/qualcomm/ftccommon/ClassManagerFactory.java @@ -32,7 +32,7 @@ import com.qualcomm.ftccommon.configuration.RobotConfigFileManager; import com.qualcomm.ftccommon.configuration.RobotConfigResFilter; -import com.qualcomm.robotcore.hardware.configuration.UserConfigurationTypeManager; +import com.qualcomm.robotcore.hardware.configuration.ConfigurationTypeManager; import org.firstinspires.ftc.robotcore.external.Supplier; import org.firstinspires.ftc.robotcore.internal.opmode.AnnotatedOpModeClassFilter; @@ -56,7 +56,7 @@ public static void registerFilters() ClassManager classManager = ClassManager.getInstance(); classManager.registerFilter(AnnotatedOpModeClassFilter.getInstance()); - classManager.registerFilter(UserConfigurationTypeManager.getInstance()); + classManager.registerFilter(ConfigurationTypeManager.getInstance()); } public static void registerResourceFilters() diff --git a/library/src/main/java/com/qualcomm/ftccommon/CommandList.java b/library/src/main/java/com/qualcomm/ftccommon/CommandList.java index 8273234..ad6b7a8 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/CommandList.java +++ b/library/src/main/java/com/qualcomm/ftccommon/CommandList.java @@ -31,11 +31,13 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE package com.qualcomm.ftccommon; +import android.support.annotation.NonNull; + import com.qualcomm.robotcore.util.SerialNumber; -import org.firstinspires.ftc.robotcore.internal.system.AppUtil; import org.firstinspires.ftc.robotcore.internal.collections.SimpleGson; import org.firstinspires.ftc.robotcore.internal.network.RobotCoreCommandList; +import org.firstinspires.ftc.robotcore.internal.system.AppUtil; import java.io.File; import java.util.ArrayList; @@ -46,11 +48,19 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE @SuppressWarnings("WeakerAccess") public class CommandList extends RobotCoreCommandList { - public static final String CMD_RESTART_ROBOT = "CMD_RESTART_ROBOT"; + //------------------------------------------------------------------------------------------------ + // Opmodes + //------------------------------------------------------------------------------------------------ public static final String CMD_INIT_OP_MODE = "CMD_INIT_OP_MODE"; public static final String CMD_RUN_OP_MODE = "CMD_RUN_OP_MODE"; + //------------------------------------------------------------------------------------------------ + // Configurations + //------------------------------------------------------------------------------------------------ + + public static final String CMD_RESTART_ROBOT = "CMD_RESTART_ROBOT"; + public static final String CMD_SCAN = "CMD_SCAN"; public static final String CMD_SCAN_RESP = "CMD_SCAN_RESP"; @@ -71,9 +81,77 @@ public class CommandList extends RobotCoreCommandList { public static final String CMD_DISCOVER_LYNX_MODULES = "CMD_DISCOVER_LYNX_MODULES"; public static final String CMD_DISCOVER_LYNX_MODULES_RESP = "CMD_DISCOVER_LYNX_MODULES_RESP"; + //------------------------------------------------------------------------------------------------ + // Networking + //------------------------------------------------------------------------------------------------ + public static final String CMD_REQUEST_REMEMBERED_GROUPS = "CMD_REQUEST_REMEMBERED_GROUPS"; public static final String CMD_REQUEST_REMEMBERED_GROUPS_RESP = "CMD_REQUEST_REMEMBERED_GROUPS_RESP"; + //------------------------------------------------------------------------------------------------ + // Sounds + //------------------------------------------------------------------------------------------------ + + public static class CmdPlaySound { + public static final String Command = "CMD_PLAY_SOUND"; + public final long msPresentationTime; + public final String hashString; + public final boolean waitForNonLoopingSoundsToFinish; + public final float volume; + public final int loopControl; + public final float rate; + + public CmdPlaySound(long msPresentationTime, String hashString, SoundPlayer.PlaySoundParams params) { + this.msPresentationTime = msPresentationTime; + this.hashString = hashString; + this.waitForNonLoopingSoundsToFinish = params.waitForNonLoopingSoundsToFinish; + this.volume = params.volume; + this.loopControl = params.loopControl; + this.rate = params.rate; + } + public String serialize() + { + return SimpleGson.getInstance().toJson(this); + } + public static CmdPlaySound deserialize(String serialized) { return SimpleGson.getInstance().fromJson(serialized, CmdPlaySound.class); } + public SoundPlayer.PlaySoundParams getParams() { + SoundPlayer.PlaySoundParams result = new SoundPlayer.PlaySoundParams(); + result.waitForNonLoopingSoundsToFinish = this.waitForNonLoopingSoundsToFinish; + result.volume = this.volume; + result.loopControl = this.loopControl; + result.rate = this.rate; + return result; + } + } + + public static class CmdRequestSound { + public static final String Command = "CMD_REQUEST_SOUND"; + public final String hashString; + public final int port; + + public CmdRequestSound(String hashString, int port) { this.hashString = hashString; this.port = port;} + public String serialize() + { + return SimpleGson.getInstance().toJson(this); + } + public static CmdRequestSound deserialize(String serialized) { return SimpleGson.getInstance().fromJson(serialized, CmdRequestSound.class); } + } + + public static class CmdStopPlayingSounds { + public static final String Command = "CMD_STOP_PLAYING_SOUNDS"; + public final SoundPlayer.StopWhat stopWhat; + public CmdStopPlayingSounds(SoundPlayer.StopWhat stopWhat) { this.stopWhat = stopWhat; } + public String serialize() + { + return SimpleGson.getInstance().toJson(this); + } + public static CmdStopPlayingSounds deserialize(String serialized) { return SimpleGson.getInstance().fromJson(serialized, CmdStopPlayingSounds.class); } + } + + //------------------------------------------------------------------------------------------------ + // Programming and management + //------------------------------------------------------------------------------------------------ + /** * Command to start programming mode (blocks). */ @@ -107,8 +185,10 @@ public class CommandList extends RobotCoreCommandList { */ public static final String CMD_STOP_PROGRAMMING_MODE = "CMD_STOP_PROGRAMMING_MODE"; + public static final String CMD_SET_MATCH_NUMBER = "CMD_SET_MATCH_NUMBER"; + //------------------------------------------------------------------------------------------------ - // Lynx firmware update suppport + // Lynx firmware update support //------------------------------------------------------------------------------------------------ public static final String CMD_GET_CANDIDATE_LYNX_FIRMWARE_IMAGES = "CMD_GET_CANDIDATE_LYNX_FIRMWARE_IMAGES"; @@ -129,7 +209,7 @@ public static LynxFirmwareImagesResp deserialize(String serialized) { } public static final String CMD_GET_USB_ACCESSIBLE_LYNX_MODULES = "CMD_GET_USB_ACCESSIBLE_LYNX_MODULES"; public static class USBAccessibleLynxModulesRequest { - public boolean includeModuleNumbers = false; + public boolean forFirmwareUpdate = false; public String serialize() { return SimpleGson.getInstance().toJson(this); @@ -196,4 +276,23 @@ public static LynxAddressChangeRequest deserialize(String serialized) { return SimpleGson.getInstance().fromJson(serialized, LynxAddressChangeRequest.class); } } + + public static class CmdVisuallyIdentify { + public static final String Command = "CMD_VISUALLY_IDENTIFY"; + public final @NonNull SerialNumber serialNumber; + public final boolean shouldIdentify; + + public CmdVisuallyIdentify(@NonNull SerialNumber serialNumber, boolean shouldIdentify) { + this.serialNumber = serialNumber; + this.shouldIdentify = shouldIdentify; + } + + public String serialize() { + return SimpleGson.getInstance().toJson(this); + } + + public static CmdVisuallyIdentify deserialize(String serialized) { + return SimpleGson.getInstance().fromJson(serialized, CmdVisuallyIdentify.class); + } + } } diff --git a/library/src/main/java/com/qualcomm/ftccommon/FtcAboutActivity.java b/library/src/main/java/com/qualcomm/ftccommon/FtcAboutActivity.java new file mode 100644 index 0000000..fe0e857 --- /dev/null +++ b/library/src/main/java/com/qualcomm/ftccommon/FtcAboutActivity.java @@ -0,0 +1,305 @@ +package com.qualcomm.ftccommon; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.support.annotation.StringRes; +import android.text.TextUtils; + +import com.qualcomm.robotcore.robocol.Command; +import com.qualcomm.robotcore.robocol.RobocolConfig; +import com.qualcomm.robotcore.util.RobotLog; +import com.qualcomm.robotcore.util.ThreadPool; +import com.qualcomm.robotcore.util.Version; +import com.qualcomm.robotcore.wifi.NetworkConnection; + +import org.firstinspires.ftc.robotcore.internal.network.CallbackResult; +import org.firstinspires.ftc.robotcore.internal.network.DeviceNameManagerFactory; +import org.firstinspires.ftc.robotcore.internal.network.NetworkConnectionHandler; +import org.firstinspires.ftc.robotcore.internal.network.RecvLoopRunnable; +import org.firstinspires.ftc.robotcore.internal.network.RobotCoreCommandList; +import org.firstinspires.ftc.robotcore.internal.system.AppUtil; +import org.firstinspires.ftc.robotcore.internal.ui.ThemedActivity; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class FtcAboutActivity extends ThemedActivity + { + //---------------------------------------------------------------------------------------------- + // State + //---------------------------------------------------------------------------------------------- + + public static final String TAG = "FtcDriverStationAboutActivity"; + @Override public String getTag() { return TAG; } + + protected final Context context = AppUtil.getDefContext(); + protected final boolean remoteConfigure = AppUtil.getInstance().isDriverStation(); + protected AboutFragment aboutFragment; + protected Future refreshFuture = null; + + final RecvLoopRunnable.RecvLoopCallback recvLoopCallback = new RecvLoopRunnable.DegenerateCallback() + { + @Override public CallbackResult commandEvent(Command command) + { + RobotLog.vv(TAG, "commandEvent: %s", command.getName()); + if (remoteConfigure) + { + switch (command.getName()) + { + case RobotCoreCommandList.CMD_REQUEST_ABOUT_INFO_RESP: { + final RobotCoreCommandList.AboutInfo aboutInfo = RobotCoreCommandList.AboutInfo.deserialize(command.getExtra()); + AppUtil.getInstance().runOnUiThread(new Runnable() + { + @Override public void run() + { + refreshRemote(aboutInfo); + } + }); + return CallbackResult.HANDLED; + } + } + } + return CallbackResult.NOT_HANDLED; + } + }; + + //---------------------------------------------------------------------------------------------- + // Life Cycle + //---------------------------------------------------------------------------------------------- + + public static CommandList.AboutInfo getLocalAboutInfo() + { + RobotCoreCommandList.AboutInfo info = new RobotCoreCommandList.AboutInfo(); + info.appVersion = getAppVersion(); + info.libVersion = Version.getLibraryVersion(); + info.buildTime = getBuildTime(); + info.networkProtocolVersion = String.format(Locale.US, "v%d", RobocolConfig.ROBOCOL_VERSION); + + NetworkConnection networkConnection = NetworkConnectionHandler.getInstance().getNetworkConnection(); + if (networkConnection != null) + { + info.networkConnectionInfo = networkConnection.getInfo(); + } + else + { + info.networkConnectionInfo = AppUtil.getDefContext().getString(R.string.unavailable); + } + return info; + } + + protected static String getAppVersion() + { + Context context = AppUtil.getDefContext(); + String appVersion; + try { + appVersion = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; + } + catch (PackageManager.NameNotFoundException e) + { + appVersion = context.getString(R.string.unavailable); + } + return appVersion; + } + + /** https://code.google.com/p/android/issues/detail?id=220039 */ + protected static String getBuildTime() + { + Context context = AppUtil.getDefContext(); + String buildTime = context.getString(R.string.unavailable); + try + { + ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0); + ZipFile zf = new ZipFile(ai.sourceDir); + ZipEntry ze = zf.getEntry("classes.dex"); + zf.close(); + long time = ze.getTime(); + buildTime = SimpleDateFormat.getInstance().format(new java.util.Date(time)); + } + catch (PackageManager.NameNotFoundException|IOException e) + { + RobotLog.ee(TAG, e, "exception determining build time"); + } + return buildTime; + } + + //---------------------------------------------------------------------------------------------- + // AboutFragment + //---------------------------------------------------------------------------------------------- + + public static class AboutFragment extends PreferenceFragment + { + protected final boolean remoteConfigure = AppUtil.getInstance().isDriverStation(); + + public void refreshLocal(RobotCoreCommandList.AboutInfo aboutInfo) + { + setPreferenceSummary(R.string.pref_app_version, aboutInfo.appVersion); + setPreferenceSummary(R.string.pref_lib_version, aboutInfo.libVersion); + setPreferenceSummary(R.string.pref_network_protocol_version, aboutInfo.networkProtocolVersion); + setPreferenceSummary(R.string.pref_build_time, aboutInfo.buildTime); + setPreferenceSummary(R.string.pref_network_connection_info, aboutInfo.networkConnectionInfo); + } + + public void refreshRemote(RobotCoreCommandList.AboutInfo aboutInfo) + { + if (remoteConfigure) + { + setPreferenceSummary(R.string.pref_app_version_rc, aboutInfo.appVersion); + setPreferenceSummary(R.string.pref_lib_version_rc, aboutInfo.libVersion); + setPreferenceSummary(R.string.pref_network_protocol_version_rc, aboutInfo.networkProtocolVersion); + setPreferenceSummary(R.string.pref_build_time_rc, aboutInfo.buildTime); + setPreferenceSummary(R.string.pref_network_connection_info_rc, aboutInfo.networkConnectionInfo); + } + } + + public void refreshAllUnavailable() + { + setPreferenceSummary(R.string.pref_app_version, null); + setPreferenceSummary(R.string.pref_lib_version, null); + setPreferenceSummary(R.string.pref_network_protocol_version, null); + setPreferenceSummary(R.string.pref_build_time, null); + setPreferenceSummary(R.string.pref_network_connection_info, null); + + setPreferenceSummary(R.string.pref_app_version_rc, null); + setPreferenceSummary(R.string.pref_lib_version_rc, null); + setPreferenceSummary(R.string.pref_network_protocol_version_rc, null); + setPreferenceSummary(R.string.pref_build_time_rc, null); + setPreferenceSummary(R.string.pref_network_connection_info_rc, null); + } + + @Override public void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + addPreferencesFromResource(com.qualcomm.ftccommon.R.xml.ftc_about_activity); + Preference prefAppCategory = findPreference(getString(R.string.pref_app_category)); + prefAppCategory.setTitle(remoteConfigure ? R.string.prefcat_about_ds : R.string.prefcat_about_rc); + + if (remoteConfigure) + { + addPreferencesFromResource(com.qualcomm.ftccommon.R.xml.ftc_about_activity_rc); + Preference prefAppCategoryRc = findPreference(getString(R.string.pref_app_category_rc)); + prefAppCategoryRc.setTitle(R.string.prefcat_about_rc); + } + + refreshAllUnavailable(); + } + + protected void setPreferenceSummary(@StringRes int idPref, String value) + { + setPreferenceSummary(AppUtil.getDefContext().getString(idPref), value); + } + + protected void setPreferenceSummary(String prefName, String value) + { + if (TextUtils.isEmpty(value)) + { + value = AppUtil.getDefContext().getString(R.string.unavailable); + } + Preference preference = findPreference(prefName); + if (preference != null) + { + preference.setSummary(value); + } + } + } + + //---------------------------------------------------------------------------------------------- + // Refreshing + //---------------------------------------------------------------------------------------------- + + protected void startRefreshing() + { + stopRefreshing(); + int msInterval = 5000; + refreshFuture = ThreadPool.getDefaultScheduler().scheduleAtFixedRate(new Runnable() + { + @Override public void run() + { + AppUtil.getInstance().runOnUiThread(new Runnable() + { + @Override public void run() + { + refresh(); + } + }); + } + }, 0, msInterval, TimeUnit.MILLISECONDS); + } + + protected void stopRefreshing() + { + if (refreshFuture != null) + { + refreshFuture.cancel(false); + refreshFuture = null; + } + } + + protected void refreshRemote(RobotCoreCommandList.AboutInfo aboutInfo) + { + aboutFragment.refreshRemote(aboutInfo); + } + + protected void refresh() + { + aboutFragment.refreshLocal(getLocalAboutInfo()); + if (remoteConfigure) + { + NetworkConnectionHandler.getInstance().sendCommand(new Command(RobotCoreCommandList.CMD_REQUEST_ABOUT_INFO)); + } + } + + //---------------------------------------------------------------------------------------------- + // Activity Life Cycle + //---------------------------------------------------------------------------------------------- + + @Override protected void onCreate(Bundle savedInstanceState) + { + RobotLog.vv(TAG, "onCreate()"); + super.onCreate(savedInstanceState); + setContentView(com.qualcomm.ftccommon.R.layout.activity_generic_settings); + + // Always make sure we have a real device name before we launch + DeviceNameManagerFactory.getInstance().initializeDeviceNameIfNecessary(); + + // Display the fragment as the main content. + aboutFragment = new AboutFragment(); + + getFragmentManager() + .beginTransaction() + .replace(android.R.id.content, aboutFragment) + .commit(); + + NetworkConnectionHandler.getInstance().pushReceiveLoopCallback(recvLoopCallback); + } + + @Override + protected void onResume() + { + super.onResume(); + startRefreshing(); + } + + @Override + protected void onPause() + { + stopRefreshing(); + super.onPause(); + } + + @Override protected void onDestroy() + { + RobotLog.vv(TAG, "onDestroy()"); + super.onDestroy(); + NetworkConnectionHandler.getInstance().removeReceiveLoopCallback(recvLoopCallback); + } + } diff --git a/library/src/main/java/com/qualcomm/ftccommon/FtcAdvancedRCSettingsActivity.java b/library/src/main/java/com/qualcomm/ftccommon/FtcAdvancedRCSettingsActivity.java index 78470ce..cb98888 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/FtcAdvancedRCSettingsActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/FtcAdvancedRCSettingsActivity.java @@ -40,8 +40,8 @@ are permitted (subject to the limitations in the disclaimer below) provided that import com.qualcomm.robotcore.util.Device; import com.qualcomm.robotcore.util.RobotLog; +import org.firstinspires.ftc.robotcore.internal.network.DeviceNameManagerFactory; import org.firstinspires.ftc.robotcore.internal.system.AppUtil; -import org.firstinspires.ftc.robotcore.internal.network.DeviceNameManager; import org.firstinspires.ftc.robotcore.internal.system.PreferencesHelper; import org.firstinspires.ftc.robotcore.internal.ui.ThemedActivity; import org.firstinspires.ftc.robotcore.internal.ui.UILocation; @@ -123,7 +123,7 @@ protected void onCreate(Bundle savedInstanceState) setContentView(R.layout.activity_generic_settings); // Always make sure we have a real device name before we launch - DeviceNameManager.getInstance().initializeDeviceNameIfNecessary(); + DeviceNameManagerFactory.getInstance().initializeDeviceNameIfNecessary(); // Display the fragment as the main content. SettingsFragment settingsFragment = new SettingsFragment(); diff --git a/library/src/main/java/com/qualcomm/ftccommon/FtcEventLoop.java b/library/src/main/java/com/qualcomm/ftccommon/FtcEventLoop.java index e3b234b..64f6294 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/FtcEventLoop.java +++ b/library/src/main/java/com/qualcomm/ftccommon/FtcEventLoop.java @@ -63,9 +63,9 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import android.app.Activity; import android.hardware.usb.UsbDevice; +import android.os.Build; import com.qualcomm.ftccommon.configuration.FtcConfigurationActivity; -import com.qualcomm.ftccommon.configuration.ScannedDevices; import com.qualcomm.ftccommon.configuration.USBScanManager; import com.qualcomm.hardware.HardwareFactory; import com.qualcomm.robotcore.eventloop.EventLoopManager; @@ -75,7 +75,9 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.qualcomm.robotcore.hardware.Gamepad; import com.qualcomm.robotcore.hardware.HardwareMap; import com.qualcomm.robotcore.hardware.LynxModuleMetaList; +import com.qualcomm.robotcore.hardware.ScannedDevices; import com.qualcomm.robotcore.hardware.configuration.Utility; +import com.qualcomm.robotcore.hardware.usb.RobotArmingStateNotifier; import com.qualcomm.robotcore.hardware.usb.RobotUsbModule; import com.qualcomm.robotcore.robocol.Command; import com.qualcomm.robotcore.robocol.TelemetryMessage; @@ -83,12 +85,15 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.qualcomm.robotcore.util.SerialNumber; import com.qualcomm.robotcore.util.ThreadPool; +import org.firstinspires.ftc.robotcore.external.ClassFactory; +import org.firstinspires.ftc.robotcore.internal.camera.CameraManagerInternal; import org.firstinspires.ftc.robotcore.internal.ftdi.FtDevice; import org.firstinspires.ftc.robotcore.internal.ftdi.FtDeviceIOException; import org.firstinspires.ftc.robotcore.internal.ftdi.FtDeviceManager; import org.firstinspires.ftc.robotcore.internal.network.CallbackResult; import org.firstinspires.ftc.robotcore.internal.opmode.OpModeManagerImpl; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -264,6 +269,8 @@ public CallbackResult processCommand(Command command) throws InterruptedExceptio handleCommandScan(extra); } else if (name.equals(CommandList.CMD_DISCOVER_LYNX_MODULES)) { handleCommandDiscoverLynxModules(extra); + } else if (name.equals(CommandList.CMD_SET_MATCH_NUMBER)) { + handleCommandSetMatchNumber(extra); } else { localResult = CallbackResult.NOT_HANDLED; } @@ -307,7 +314,7 @@ protected void handleCommandScan(String extra) throws RobotCoreException, Interr protected void handleCommandDiscoverLynxModules(String extra) throws RobotCoreException, InterruptedException { RobotLog.vv(FtcConfigurationActivity.TAG, "handling command DiscoverLynxModules"); - final SerialNumber serialNumber = new SerialNumber(extra); + final SerialNumber serialNumber = SerialNumber.fromString(extra); final USBScanManager usbScanManager = startUsbScanMangerIfNecessary(); @@ -349,6 +356,19 @@ protected void sendUIState() { if (manager != null) manager.refreshSystemTelemetryNow(); // null check is paranoia, need isn't verified } + /* + * handleCommandSetMatchNumber + * + * Cache the match number in the opMode manager. + */ + protected void handleCommandSetMatchNumber(String extra) { + try { + opModeManager.setMatchNumber(Integer.parseInt(extra)); + } catch (NumberFormatException e) { + RobotLog.logStackTrace(e); + } + } + protected void handleCommandInitOpMode(String extra) { String newOpMode = ftcEventLoopHandler.getOpMode(extra); opModeManager.initActiveOpMode(newOpMode); @@ -404,32 +424,53 @@ private void processOpModeStopRequest(OpMode opModeToStop) { // FT_Device for which we're actually receiving a change notification: who else // would have it open (for example)? We'd like to do something more, but don't // have an idea of what that would look like. + // + // 2018.06.01: It is suspected that the serial number being returned as null was + // a consequence of a race between USB attachment notifications here and in FtDeviceManager. RobotLog.ee(TAG, "ignoring: unable get serial number of attached UsbDevice vendor=0x%04x, product=0x%04x device=0x%04x name=%s", usbDevice.getVendorId(), usbDevice.getProductId(), usbDevice.getDeviceId(), usbDevice.getDeviceName()); } } protected SerialNumber getSerialNumberOfUsbDevice(UsbDevice usbDevice) { - FtDevice ftDevice = null; SerialNumber serialNumber = null; - try { - FtDeviceManager manager = FtDeviceManager.getInstance(this.activityContext); // note: we're not supposed to close this - ftDevice = manager.openByUsbDevice(this.activityContext, usbDevice); - if (ftDevice != null) { - serialNumber = new SerialNumber(ftDevice.getDeviceInfo().serialNumber); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + serialNumber = SerialNumber.fromStringOrNull(usbDevice.getSerialNumber()); + } + + if (serialNumber==null) { + // Don't need this branch any more, but left in for now to preserve code paths. Remove after further testing. + FtDevice ftDevice = null; + try { + FtDeviceManager manager = FtDeviceManager.getInstance(this.activityContext); // note: we're not supposed to close this + ftDevice = manager.openByUsbDevice(this.activityContext, usbDevice); + if (ftDevice != null) { + serialNumber = SerialNumber.fromStringOrNull(ftDevice.getDeviceInfo().serialNumber); + } + } catch (RuntimeException|FtDeviceIOException e) { // RuntimeException is paranoia + // ignored + } finally { + if (ftDevice != null) + ftDevice.close(); } - } catch (RuntimeException|FtDeviceIOException e) { // RuntimeException is paranoia - // ignored - } finally { - if (ftDevice != null) - ftDevice.close(); } + + if (serialNumber==null) { // non FTDI devices on KitKat, or devices that simply lack a serial number + try { + CameraManagerInternal cameraManagerInternal = (CameraManagerInternal) ClassFactory.getInstance().getCameraManager(); + serialNumber = cameraManagerInternal.getRealOrVendorProductSerialNumber(usbDevice); + } catch (RuntimeException e) { + // ignore + } + } + return serialNumber; } @Override public void pendUsbDeviceAttachment(SerialNumber serialNumber, long time, TimeUnit unit) { long nsDeadline = time==0L ? 0L : System.nanoTime() + unit.toNanos(time); - this.recentlyAttachedUsbDevices.put(serialNumber.toString(), nsDeadline); + this.recentlyAttachedUsbDevices.put(serialNumber.getString(), nsDeadline); } /** @@ -456,13 +497,22 @@ protected SerialNumber getSerialNumberOfUsbDevice(UsbDevice usbDevice) { List modules = this.ftcEventLoopHandler.getHardwareMap().getAll(RobotUsbModule.class); // For each serial number, find the module with that serial number and ask the handler to deal with it - for (String serialNumber : serialNumbersToProcess) { + for (String serialNumberString : new ArrayList<>(serialNumbersToProcess)) { + SerialNumber serialNumberAttached = SerialNumber.fromString(serialNumberString); + boolean found = false; for (RobotUsbModule module : modules) { - if (module.getSerialNumber().toString().equals(serialNumber)) { - handleUsbModuleAttach(module); - break; + if (serialNumberAttached.matches(module.getSerialNumber())) { + if (module.getArmingState() != RobotArmingStateNotifier.ARMINGSTATE.ARMED) { + serialNumbersToProcess.remove(serialNumberString); + handleUsbModuleAttach(module); + found = true; + break; + } } } + if (!found) { + RobotLog.vv(TAG, "processedRecentlyAttachedUsbDevices(): %s not in hwmap; ignoring", serialNumberAttached); + } } } } diff --git a/library/src/main/java/com/qualcomm/ftccommon/FtcEventLoopBase.java b/library/src/main/java/com/qualcomm/ftccommon/FtcEventLoopBase.java index bf382e6..1739a38 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/FtcEventLoopBase.java +++ b/library/src/main/java/com/qualcomm/ftccommon/FtcEventLoopBase.java @@ -99,7 +99,8 @@ are permitted (subject to the limitations in the disclaimer below) provided that import com.qualcomm.ftccommon.configuration.FtcConfigurationActivity; import com.qualcomm.ftccommon.configuration.RobotConfigFile; import com.qualcomm.ftccommon.configuration.RobotConfigFileManager; -import com.qualcomm.ftccommon.configuration.ScannedDevices; +import com.qualcomm.robotcore.hardware.VisuallyIdentifiableHardwareDevice; +import com.qualcomm.robotcore.hardware.ScannedDevices; import com.qualcomm.ftccommon.configuration.USBScanManager; import com.qualcomm.hardware.HardwareDeviceManager; import com.qualcomm.hardware.HardwareFactory; @@ -115,11 +116,10 @@ are permitted (subject to the limitations in the disclaimer below) provided that import com.qualcomm.robotcore.hardware.DeviceManager; import com.qualcomm.robotcore.hardware.LynxModuleMeta; import com.qualcomm.robotcore.hardware.LynxModuleMetaList; -import com.qualcomm.robotcore.hardware.RobotCoreLynxUsbDevice; import com.qualcomm.robotcore.hardware.configuration.ControllerConfiguration; import com.qualcomm.robotcore.hardware.configuration.LynxConstants; import com.qualcomm.robotcore.hardware.configuration.ReadXMLFileHandler; -import com.qualcomm.robotcore.hardware.configuration.UserConfigurationTypeManager; +import com.qualcomm.robotcore.hardware.configuration.ConfigurationTypeManager; import com.qualcomm.robotcore.hardware.configuration.WriteXMLFileHandler; import com.qualcomm.robotcore.hardware.usb.RobotUsbDevice; import com.qualcomm.robotcore.hardware.usb.RobotUsbManager; @@ -130,6 +130,7 @@ are permitted (subject to the limitations in the disclaimer below) provided that import com.qualcomm.robotcore.util.ThreadPool; import org.firstinspires.ftc.robotcore.external.Consumer; +import org.firstinspires.ftc.robotcore.external.function.Supplier; import org.firstinspires.ftc.robotcore.internal.collections.SimpleGson; import org.firstinspires.ftc.robotcore.internal.network.CallbackResult; import org.firstinspires.ftc.robotcore.internal.network.NetworkConnectionHandler; @@ -272,6 +273,10 @@ else if (name.equals(CommandList.CMD_REQUEST_INSPECTION_REPORT)) { handleCommandRequestInspectionReport(); } + else if (name.equals(CommandList.CMD_REQUEST_ABOUT_INFO)) + { + handleCommandRequestAboutInfo(command); + } else if (name.equals(CommandList.CMD_DISCONNECT_FROM_WIFI_DIRECT)) { handleCommandDisconnectWifiDirect(); @@ -340,6 +345,22 @@ else if (name.equals(CommandList.CMD_ROBOT_CONTROLLER_PREFERENCE)) { result = PreferenceRemoterRC.getInstance().handleCommandRobotControllerPreference(extra); } + else if (name.equals(CommandList.CmdPlaySound.Command)) + { + result = SoundPlayer.getInstance().handleCommandPlaySound(extra); + } + else if (name.equals(CommandList.CmdRequestSound.Command)) + { + result = SoundPlayer.getInstance().handleCommandRequestSound(command); + } + else if (name.equals(CommandList.CmdStopPlayingSounds.Command)) + { + result = SoundPlayer.getInstance().handleCommandStopPlayingSounds(command); + } + else if (name.equals(CommandList.CmdVisuallyIdentify.Command)) + { + result = handleCommandVisuallyIdentify(command); + } else { result = CallbackResult.NOT_HANDLED; @@ -361,7 +382,7 @@ protected void sendUIState() networkConnectionHandler.sendCommand(new Command(CommandList.CMD_NOTIFY_ACTIVE_CONFIGURATION, serialized)); // Send the user device type list - UserConfigurationTypeManager.getInstance().sendUserDeviceTypes(); + ConfigurationTypeManager.getInstance().sendUserDeviceTypes(); // We might get a request in really soon, before we're fully together. Wait: the driver // station doesn't retry if we were to ignore (might not need any more, as we send this @@ -432,7 +453,7 @@ protected void handleCommandRequestParticularConfiguration(String data) } catch (RobotCoreException e) { - e.printStackTrace(); + RobotLog.logStackTrace(e); } } @@ -533,56 +554,47 @@ protected boolean updateLynxFirmware(SerialNumber serialNumber, final CommandLis final LynxUsbDeviceContainer lynxUsbDevice = getLynxUsbDeviceForFirmwareUpdate(serialNumber); if (lynxUsbDevice != null) { - byte[] firmwareImage = ReadWriteFile.readBytes(imageFileName); - if (firmwareImage.length > 0) - { - lynxUsbDevice.disengage(); - try { - enterFirmwareUpdateMode(lynxUsbDevice.getRobotUsbDevice()); - // - FlashLoaderManager manager = new FlashLoaderManager(lynxUsbDevice.getRobotUsbDevice(), firmwareImage); + try { + byte[] firmwareImage = ReadWriteFile.readBytes(imageFileName); + if (firmwareImage.length > 0) + { + RobotLog.vv(TAG, "disengaging lynx usb device %s", lynxUsbDevice.getSerialNumber()); + lynxUsbDevice.disengage(); try { - manager.updateFirmware(new Consumer() + // Try the update few times, in the hope of mitigating transient errors. Each time + // we reset the hub and toggle it to enter programming mode, so it will at least + // pay attention to us and try to cooperate, even after a failed update. + int cRetryFirmwareUpdate = 4; + for (int i = 0; i < cRetryFirmwareUpdate; i++) { - Double prevPercentComplete = null; - @Override public void accept(ProgressParameters parameters) + RobotLog.vv(TAG, "trying firmware update: count=%d", i); + if (updateFirmwareOnce(lynxUsbDevice, imageFileName.getName(), firmwareImage, serialNumber)) { - double percentComplete = Math.round(parameters.fractionComplete() * 100); - if (prevPercentComplete==null || prevPercentComplete != percentComplete) - { - prevPercentComplete = percentComplete; - AppUtil.getInstance().showProgress(UILocation.BOTH, - String.format(activityContext.getString(R.string.expansionHubFirmwareUpdateMessage), lynxUsbDevice.getSerialNumber(), imageFileName.getName()), - parameters.fractionComplete(), - 100); - } + break; // success } - }); - } - catch (InterruptedException e) - { - success = false; - Thread.currentThread().interrupt(); - } - catch (TimeoutException|FlashLoaderProtocolException|IOException e) - { - success = false; - RobotLog.logExceptionHeader(TAG, e, "exception while updating firmware: serial=%s", serialNumber); - RobotLog.logStacktrace(e); + } } finally { - AppUtil.getInstance().dismissProgress(UILocation.BOTH); + RobotLog.vv(TAG, "reengaging lynx usb device %s", lynxUsbDevice.getSerialNumber()); + lynxUsbDevice.engage(); } } - finally + else { - RobotLog.vv(TAG, "updateLynxFirmware: cleaning up..."); - lynxUsbDevice.engage(); - lynxUsbDevice.close(); - RobotLog.vv(TAG, "...updateLynxFirmware: cleaning up"); + success = false; + RobotLog.ee(TAG, "firmware image file unexpectedly empty"); } } + finally + { + lynxUsbDevice.close(); + } + } + else + { + success = false; + RobotLog.ee(TAG, "unable to obtain lynx usb device for fw update: %s", serialNumber); } } catch (RuntimeException e) @@ -594,18 +606,77 @@ protected boolean updateLynxFirmware(SerialNumber serialNumber, final CommandLis return success; } - protected void enterFirmwareUpdateMode(RobotUsbDevice robotUsbDevice) + protected boolean updateFirmwareOnce(final LynxUsbDeviceContainer lynxUsbDevice, final String imageFileName, byte[] firmwareImage, SerialNumber serialNumber) + { + boolean success = true; + if (enterFirmwareUpdateMode(lynxUsbDevice.getRobotUsbDevice())) + { + FlashLoaderManager manager = new FlashLoaderManager(lynxUsbDevice.getRobotUsbDevice(), firmwareImage); + try { + manager.updateFirmware(new Consumer() + { + Double prevPercentComplete = null; + @Override public void accept(ProgressParameters parameters) + { + double percentComplete = Math.round(parameters.fractionComplete() * 100); + if (prevPercentComplete==null || prevPercentComplete != percentComplete) + { + prevPercentComplete = percentComplete; + AppUtil.getInstance().showProgress(UILocation.BOTH, + String.format(activityContext.getString(R.string.expansionHubFirmwareUpdateMessage), lynxUsbDevice.getSerialNumber(), imageFileName), + parameters.fractionComplete(), + 100); + } + } + }); + } + catch (InterruptedException e) + { + success = false; + Thread.currentThread().interrupt(); + RobotLog.ee(TAG, "interrupt while updating firmware: serial=%s", serialNumber); + } + catch (FlashLoaderProtocolException e) + { + success = false; + RobotLog.ee(TAG, e, "exception while updating firmware: serial=%s", serialNumber); + } + finally + { + AppUtil.getInstance().dismissProgress(UILocation.BOTH); + } + } + else + { + RobotLog.ee(TAG, "failed to enter firmware update mode"); + } + return success; + } + + protected boolean enterFirmwareUpdateMode(RobotUsbDevice robotUsbDevice) { + boolean result = false; if (LynxConstants.isEmbeddedSerialNumber(robotUsbDevice.getSerialNumber())) { RobotLog.vv(TAG, "putting embedded lynx into firmware update mode"); - LynxUsbDeviceImpl.enterFirmwareUpdateModeDragonboardCombo(); + result = LynxUsbDeviceImpl.enterFirmwareUpdateModeDragonboardCombo(); } else { RobotLog.vv(TAG, "putting lynx(serial=%s) into firmware update mode", robotUsbDevice.getSerialNumber()); - LynxUsbDeviceImpl.enterFirmwareUpdateModeUSB(robotUsbDevice); + result = LynxUsbDeviceImpl.enterFirmwareUpdateModeUSB(robotUsbDevice); } + + // Sleep a bit to give the Lynx module time to enter bootloader. Actual time spent is a wild guess. + try { + Thread.sleep(100); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + } + + return result; } protected void handleCommandGetUSBAccessibleLynxModules(final Command commandRequest) @@ -617,7 +688,7 @@ protected void handleCommandGetUSBAccessibleLynxModules(final Command commandReq CommandList.USBAccessibleLynxModulesRequest request = CommandList.USBAccessibleLynxModulesRequest.deserialize(commandRequest.getExtra()); ArrayList modules = new ArrayList(); try { - modules.addAll(getUSBAccessibleLynxDevices(request.includeModuleNumbers)); + modules.addAll(getUSBAccessibleLynxDevices(request.forFirmwareUpdate)); } catch (RobotCoreException ignored) { @@ -626,7 +697,7 @@ protected void handleCommandGetUSBAccessibleLynxModules(final Command commandReq { @Override public int compare(USBAccessibleLynxModule lhs, USBAccessibleLynxModule rhs) { - return lhs.getSerialNumber().toString().compareTo(rhs.getSerialNumber().toString()); + return lhs.getSerialNumber().getString().compareTo(rhs.getSerialNumber().getString()); } }); CommandList.USBAccessibleLynxModulesResp resp = new CommandList.USBAccessibleLynxModulesResp(); @@ -643,19 +714,21 @@ protected LynxUsbDeviceContainer getLynxUsbDeviceForFirmwareUpdate(SerialNumber { if (lynxUsbDevice.getSerialNumber().equals(serialNumber)) { - return new LynxUsbDeviceContainer(lynxUsbDevice); + RobotLog.vv(TAG, "getLynxUsbDeviceForFirmwareUpdate(): found existing %s", serialNumber); + return new LynxUsbDeviceContainer(lynxUsbDevice, serialNumber); } } // No, then open it try { + RobotLog.vv(TAG, "getLynxUsbDeviceForFirmwareUpdate(): opening %s", serialNumber); RobotUsbManager robotUsbManager = HardwareDeviceManager.createUsbManager(AppUtil.getDefContext()); - RobotUsbDevice robotUsbDevice = LynxUsbUtil.openUsbDevice(robotUsbManager, serialNumber); - return new LynxUsbDeviceContainer(robotUsbDevice); + RobotUsbDevice robotUsbDevice = LynxUsbUtil.openUsbDevice(true, robotUsbManager, serialNumber); + return new LynxUsbDeviceContainer(robotUsbDevice, serialNumber); } catch (RobotCoreException e) { - // ignored; + RobotLog.ee(TAG, e, "getLynxUsbDeviceForFirmwareUpdate(): exception opening lynx usb device: %s", serialNumber); } return null; @@ -665,23 +738,27 @@ protected LynxUsbDeviceContainer getLynxUsbDeviceForFirmwareUpdate(SerialNumber protected static class LynxUsbDeviceContainer { protected final LynxUsbDeviceImpl lynxUsbDevice; - protected final RobotUsbDevice robotUsbDevice; + protected final RobotUsbDevice robotUsbDevice; // if non-null, close on close + protected final SerialNumber serialNumber; - public LynxUsbDeviceContainer(@NonNull LynxUsbDeviceImpl lynxUsbDevice) + public LynxUsbDeviceContainer(@NonNull LynxUsbDeviceImpl lynxUsbDevice, SerialNumber serialNumber) // existing open { this.lynxUsbDevice = lynxUsbDevice; this.robotUsbDevice = null; + this.serialNumber = serialNumber; } - public LynxUsbDeviceContainer(@NonNull RobotUsbDevice robotUsbDevice) + public LynxUsbDeviceContainer(@NonNull RobotUsbDevice robotUsbDevice, SerialNumber serialNumber) // newly opened { this.lynxUsbDevice = null; this.robotUsbDevice = robotUsbDevice; + this.serialNumber = serialNumber; } public void close() { try { if (robotUsbDevice != null) { + RobotLog.vv(TAG, "getLynxUsbDeviceForFirmwareUpdate(): closing %s", serialNumber); robotUsbDevice.requestReadInterrupt(true); robotUsbDevice.close(); } @@ -711,14 +788,14 @@ public SerialNumber getSerialNumber() } } - protected List getUSBAccessibleLynxDevices(boolean includeModuleAddresses) throws RobotCoreException + protected List getUSBAccessibleLynxDevices(boolean forFirmwareUpdate) throws RobotCoreException { - RobotLog.vv(TAG, "getUSBAccessibleLynxDevices()..."); + RobotLog.vv(TAG, "getUSBAccessibleLynxDevices(includeModuleAddresses=%s)...", forFirmwareUpdate); // We do a raw, low level scan, not caring what's in the current hardware map, if anything. // This is important: a module might, for example, be in a state where it previously had a // failed firmware update, and all that's running is its bootloader. Such a beast would be - // unable to respond to + // unable to respond to a high level scan. USBScanManager scanManager = startUsbScanMangerIfNecessary(); final ThreadPool.SingletonResult future = scanManager.startDeviceScanIfNecessary(); try { @@ -726,20 +803,17 @@ protected List getUSBAccessibleLynxDevices(boolean incl List result = new ArrayList(); // Return everything returned by the scan - for (Map.Entry entry : scannedDevices.entrySet()) + for (Map.Entry entry : scannedDevices.entrySet()) { - if (entry.getValue() == DeviceManager.DeviceType.LYNX_USB_DEVICE) + if (entry.getValue() == DeviceManager.UsbDeviceType.LYNX_USB_DEVICE) { SerialNumber serialNumber = entry.getKey(); - // For the moment, serial numbers of the embedded module must be one. If the - // embedded/synthetic module was discovered rather than assuming its address - // to always one, this could be relaxed. - result.add(new USBAccessibleLynxModule(serialNumber, !serialNumber.equals(LynxConstants.SERIAL_NUMBER_EMBEDDED))); + result.add(new USBAccessibleLynxModule(serialNumber, true)); } } - // Return the embedded module if we're supposed to and if it wasn't already there (which it will be, I think, always, now) - if (LynxConstants.enableLynxFirmwareUpdateForDragonboard()) + // Return the embedded module if we're supposed to and if it wasn't already there (it might be absent if it's bricked) + if (LynxConstants.isRevControlHub()) { boolean found = false; for (USBAccessibleLynxModule module : result) @@ -752,51 +826,105 @@ protected List getUSBAccessibleLynxDevices(boolean incl } if (!found) { - result.add(new USBAccessibleLynxModule(LynxConstants.SERIAL_NUMBER_EMBEDDED, false)); + result.add(new USBAccessibleLynxModule(LynxConstants.SERIAL_NUMBER_EMBEDDED, true)); } } - // Add module addresses if asked - if (includeModuleAddresses) + for (USBAccessibleLynxModule module : result) { + RobotLog.vv(TAG, "getUSBAccessibleLynxDevices: found serial=%s", module.getSerialNumber()); + } + + // Add additional information if we're asked to + if (forFirmwareUpdate) + { + RobotLog.vv(TAG, "finding module addresses and current firmware versions"); for (int i = 0; i < result.size(); ) { - USBAccessibleLynxModule usbModule = result.get(i); - RobotCoreLynxUsbDevice device = scanManager.getDeviceManager().createLynxUsbDevice(usbModule.getSerialNumber(), null); + final USBAccessibleLynxModule usbModule = result.get(i); + RobotLog.vv(TAG, "getUSBAccessibleLynxDevices: finding module address for usbModule %s", usbModule.getSerialNumber()); + LynxUsbDevice lynxUsbDevice = (LynxUsbDevice) scanManager.getDeviceManager().createLynxUsbDevice(usbModule.getSerialNumber(), null); try { - LynxModuleMetaList lynxModuleMetas = device.discoverModules(); + // Discover all the modules on us, sure, but we're really only interested in + // *our* address. At the same time, we'd like to talk to the lynx module on + // lynxUsbDevice while it's already open, just for a moment. Finally, be aware + // that when we're talking to a bricked hub that discovery comes back empty. + LynxModuleMetaList lynxModuleMetas = lynxUsbDevice.discoverModules(); + + // Find *our* module address in the metadata, if we can. Further, if there + // were states or configurations that a module might be in which it was ineligible + // for firmware update, we should check for them here. Historically, we thought that + // that might include having a child module attached, but testing and a bit of more + // careful thought shows that having a child isn't a problem. boolean foundParent = false; - boolean foundChild = false; + usbModule.setModuleAddress(0); for (LynxModuleMeta meta : lynxModuleMetas) { - if (meta.getModuleAddress()==0) continue; // paranoia + RobotLog.vv(TAG,"assessing %s", meta); + if (meta.getModuleAddress()==0) // paranoia + { + RobotLog.vv(TAG, "ignoring module with address zero"); + continue; + } if (meta.isParent()) { - usbModule.setModuleAddress(meta.getModuleAddress()); + // It's us! foundParent = true; + usbModule.setModuleAddress(meta.getModuleAddress()); } else { - // We've got child modules connected: these are unsafe to update - foundChild = true; + // We've got child modules connected + } + } + + // As of this writing, there's no known reason we shouldn't try to update any + // module that we in fact happen to run across. + boolean okToUpdateFirmware = true; + + // Find his *current* fw version if we can + usbModule.setFirmwareVersionString(""); + if (okToUpdateFirmware && foundParent) + { + try { + talkToParentLynxModule(scanManager.getDeviceManager(), lynxUsbDevice, usbModule.getModuleAddress(), new Consumer() + { + @Override public void accept(LynxModule lynxModule) + { + String fw = lynxModule.getNullableFirmwareVersionString(); + if (fw != null) + { + usbModule.setFirmwareVersionString(fw); + } + else + RobotLog.ee(TAG, "getUSBAccessibleLynxDevices(): fw returned null"); + } + }); + } + catch (RobotCoreException|LynxNackException e) + { + RobotLog.ee(TAG, e, "exception retrieving fw version; ignoring"); } } - if (foundParent && !foundChild) - i++; + + if (okToUpdateFirmware) + { + i++; // advance to next usb accessible module + } else { - RobotLog.vv(TAG, "lynx module %s not actually accessible", usbModule.getSerialNumber()); + RobotLog.vv(TAG, "getUSBAccessibleLynxDevices: culled serial=%s", usbModule.getSerialNumber()); result.remove(i); } } finally { - if (device != null) device.close(); + if (lynxUsbDevice != null) lynxUsbDevice.close(); } } } - RobotLog.vv(TAG, "...getUSBAccessibleLynxDevices(): %d modules found", result.size()); + RobotLog.vv(TAG, "getUSBAccessibleLynxDevices(): %d modules found", result.size()); return result; } catch (InterruptedException e) @@ -804,8 +932,14 @@ protected List getUSBAccessibleLynxDevices(boolean incl Thread.currentThread().interrupt(); return new ArrayList(); } + finally + { + RobotLog.vv(TAG, "...getUSBAccessibleLynxDevices()"); + } } + + protected void handleCommandLynxChangeModuleAddresses(final Command commandRequest) { ThreadPool.getDefault().execute(new Runnable() @@ -818,28 +952,25 @@ protected void handleCommandLynxChangeModuleAddresses(final Command commandReque CommandList.LynxAddressChangeRequest changeRequest = CommandList.LynxAddressChangeRequest.deserialize(commandRequest.getExtra()); USBScanManager scanManager = startUsbScanMangerIfNecessary(); DeviceManager deviceManager = scanManager.getDeviceManager(); - for (CommandList.LynxAddressChangeRequest.AddressChange addressChange : changeRequest.modulesToChange) + for (final CommandList.LynxAddressChangeRequest.AddressChange addressChange : changeRequest.modulesToChange) { LynxUsbDevice lynxUsbDevice = (LynxUsbDevice)deviceManager.createLynxUsbDevice(addressChange.serialNumber, null); try { - LynxModule lynxModule = (LynxModule)deviceManager.createLynxModule(lynxUsbDevice, addressChange.oldAddress, true, null); - lynxModule.setUserModule(false); - lynxUsbDevice.addConfiguredModule(lynxModule); - try { - RobotLog.vv(TAG, "lynx module %s: change address %d -> %d", addressChange.serialNumber, addressChange.oldAddress, addressChange.newAddress); - lynxModule.setNewModuleAddress(addressChange.newAddress); - } - finally + talkToParentLynxModule(deviceManager, lynxUsbDevice, addressChange.oldAddress, new Consumer() { - lynxModule.removeAsConfigured(); - lynxModule.close(); - } + @Override public void accept(LynxModule lynxModule) + { + RobotLog.vv(TAG, "lynx module %s: change address %d -> %d", addressChange.serialNumber, addressChange.oldAddress, addressChange.newAddress); + lynxModule.setNewModuleAddress(addressChange.newAddress); + } + }); } - catch (RobotCoreException|LynxNackException ignored) + catch (RobotCoreException|LynxNackException e) { + RobotLog.ee(TAG, e, "failure during module address change"); AppUtil.getInstance().showToast(UILocation.BOTH, activityContext.getString(R.string.toastLynxAddressChangeFailed, addressChange.serialNumber)); success = false; - throw ignored; + throw e; } finally { @@ -861,9 +992,33 @@ protected void handleCommandLynxChangeModuleAddresses(final Command commandReque } } }); + } + protected void talkToParentLynxModule(DeviceManager deviceManager, LynxUsbDevice lynxUsbDevice, int moduleAddress, Consumer consumer) throws RobotCoreException, InterruptedException, LynxNackException + { + // Two cases: (a) the usb device is already alive and running, having been opened + // in the hwmap: just ask it for the module (b) the device is not in the hw map and + // so not really open: create our own temporary LynxModule - + LynxModule lynxModule = lynxUsbDevice.getConfiguredModule(moduleAddress); + if (lynxModule != null) + { + consumer.accept(lynxModule); + } + else + { + lynxModule = (LynxModule)deviceManager.createLynxModule(lynxUsbDevice, moduleAddress, true, null); + lynxModule.setUserModule(false); + lynxUsbDevice.addConfiguredModule(lynxModule); + try { + consumer.accept(lynxModule); + } + finally + { + lynxModule.removeAsConfigured(); + lynxModule.close(); + } + } } protected void handleCommandGetCandidateLynxFirmwareImages(Command commandRequest) @@ -955,7 +1110,9 @@ protected void handleCommandStopProgrammingMode() protected void handleCommandShowDialog(Command command) { RobotCoreCommandList.ShowDialog showDialog = RobotCoreCommandList.ShowDialog.deserialize(command.getExtra()); - AppUtil.getInstance().showAlertDialog(showDialog.uuidString, UILocation.ONLY_LOCAL, showDialog.title, showDialog.message); + AppUtil.DialogParams params = new AppUtil.DialogParams(UILocation.ONLY_LOCAL, showDialog.title, showDialog.message); + params.uuidString = showDialog.uuidString; + AppUtil.getInstance().showDialog(params); } protected void handleCommandDismissDialog(Command command) @@ -990,11 +1147,25 @@ protected void handleCommandShowToast(Command command) protected void handleCommandRequestInspectionReport() { InspectionState inspectionState = new InspectionState(); - inspectionState.initializeLocal(); + try + { + inspectionState.initializeLocal(ftcEventLoopHandler.getHardwareMap()); + } + catch (RobotCoreException | InterruptedException e) + { + e.printStackTrace(); + } String serialized = inspectionState.serialize(); networkConnectionHandler.sendCommand(new Command(CommandList.CMD_REQUEST_INSPECTION_REPORT_RESP, serialized)); } + protected void handleCommandRequestAboutInfo(Command command) + { + RobotCoreCommandList.AboutInfo aboutInfo = FtcAboutActivity.getLocalAboutInfo(); + String serialized = aboutInfo.serialize(); + networkConnectionHandler.sendCommand(new Command(CommandList.CMD_REQUEST_ABOUT_INFO_RESP, serialized)); + } + protected void handleCommandDisconnectWifiDirect() { if (WifiDirectAgent.getInstance().disconnectFromWifiDirect()) @@ -1006,4 +1177,30 @@ protected void handleCommandDisconnectWifiDirect() AppUtil.getInstance().showToast(UILocation.BOTH, AppUtil.getDefContext().getString(R.string.toastErrorDisconnectingFromWifiDirect)); } } + + protected CallbackResult handleCommandVisuallyIdentify(Command command) + { + final CommandList.CmdVisuallyIdentify cmdVisuallyIdentify = CommandList.CmdVisuallyIdentify.deserialize(command.getExtra()); + ThreadPool.getDefaultSerial().execute(new Runnable() { // serial so that 'off' identifies don't re-order + @Override public void run() { + VisuallyIdentifiableHardwareDevice visuallyIdentifiable = ftcEventLoopHandler.getHardwareDevice( + VisuallyIdentifiableHardwareDevice.class, + cmdVisuallyIdentify.serialNumber, + new Supplier() { + @Override public USBScanManager get() { + try { + return startUsbScanMangerIfNecessary(); + } catch (RobotCoreException e) { + RobotLog.ee(TAG, e, "exception scanning USB in handleCommandVisuallyIdentify()"); + } + return null; + } + }); + if (visuallyIdentifiable != null) { + visuallyIdentifiable.visuallyIdentify(cmdVisuallyIdentify.shouldIdentify); + } + } + }); + return CallbackResult.HANDLED; + } } diff --git a/library/src/main/java/com/qualcomm/ftccommon/FtcEventLoopHandler.java b/library/src/main/java/com/qualcomm/ftccommon/FtcEventLoopHandler.java index b0792cc..3b33155 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/FtcEventLoopHandler.java +++ b/library/src/main/java/com/qualcomm/ftccommon/FtcEventLoopHandler.java @@ -32,7 +32,9 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE package com.qualcomm.ftccommon; import android.content.Context; +import android.support.annotation.Nullable; +import com.qualcomm.ftccommon.configuration.USBScanManager; import com.qualcomm.hardware.HardwareFactory; import com.qualcomm.hardware.lynx.LynxUsbDevice; import com.qualcomm.hardware.lynx.LynxUsbDeviceImpl; @@ -43,8 +45,12 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.qualcomm.robotcore.hardware.Gamepad; import com.qualcomm.robotcore.hardware.HardwareDeviceCloseOnTearDown; import com.qualcomm.robotcore.hardware.HardwareMap; +import com.qualcomm.robotcore.hardware.ScannedDevices; import com.qualcomm.robotcore.hardware.ServoController; import com.qualcomm.robotcore.hardware.VoltageSensor; +import com.qualcomm.robotcore.hardware.configuration.ConfigurationUtility; +import com.qualcomm.robotcore.hardware.configuration.ControllerConfiguration; +import com.qualcomm.robotcore.hardware.configuration.LynxConstants; import com.qualcomm.robotcore.hardware.usb.RobotArmingStateNotifier; import com.qualcomm.robotcore.robocol.TelemetryMessage; import com.qualcomm.robotcore.robot.RobotState; @@ -52,6 +58,9 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.qualcomm.robotcore.util.ElapsedTime; import com.qualcomm.robotcore.util.MovingStatistics; import com.qualcomm.robotcore.util.RobotLog; +import com.qualcomm.robotcore.util.SerialNumber; + +import org.firstinspires.ftc.robotcore.external.function.Supplier; import java.util.ArrayList; import java.util.List; @@ -65,10 +74,12 @@ public class FtcEventLoopHandler implements BatteryChecker.BatteryWatcher { public static final String TAG = "FtcEventLoopHandler"; - /** This string is sent in the robot battery telemetry payload to indicate + /** This string is sent in the robot battery telemetry payload to identify * that no voltage sensor is available on the robot. */ public static final String NO_VOLTAGE_SENSOR = "$no$voltage$sensor$"; + protected static final boolean DEBUG = false; + //------------------------------------------------------------------------------------------------ // State //------------------------------------------------------------------------------------------------ @@ -85,7 +96,7 @@ public class FtcEventLoopHandler implements BatteryChecker.BatteryWatcher { protected ElapsedTime robotBatteryTimer = new ElapsedTime(); protected double robotBatteryInterval = 3.00; // in seconds protected MovingStatistics robotBatteryStatistics = new MovingStatistics(10); - protected ElapsedTime robotBatteryLoggingTimer = new ElapsedTime(0); // 0 so we get an initial report + protected ElapsedTime robotBatteryLoggingTimer = null; protected double robotBatteryLoggingInterval = robotControllerBatteryCheckerInterval; protected ElapsedTime userTelemetryTimer = new ElapsedTime(0); // 0 so we get an initial report @@ -95,7 +106,10 @@ public class FtcEventLoopHandler implements BatteryChecker.BatteryWatcher { protected ElapsedTime updateUITimer = new ElapsedTime(); protected double updateUIInterval = 0.250; // in seconds - protected HardwareMap hardwareMap = null; + /** the actual hardware map seen by the user */ + protected HardwareMap hardwareMap = null; + /** the hardware map in which we keep any extra devices (ones not used by the user) we need to instantiate */ + protected HardwareMap hardwareMapExtra = null; //------------------------------------------------------------------------------------------------ // Construction @@ -107,7 +121,8 @@ public FtcEventLoopHandler(HardwareFactory hardwareFactory, UpdateUI.Callback ca this.robotControllerContext = robotControllerContext; long milliseconds = (long)(robotControllerBatteryCheckerInterval * 1000); //milliseconds - robotControllerBatteryChecker = new BatteryChecker(robotControllerContext, this, milliseconds); + robotControllerBatteryChecker = new BatteryChecker(this, milliseconds); + if (DEBUG) robotBatteryLoggingTimer = new ElapsedTime(0); } //------------------------------------------------------------------------------------------------ @@ -120,14 +135,10 @@ public void init(EventLoopManager eventLoopManager) { } public void close() { - // Close motor and servo controllers first, since some of them may reside on top - // of legacy modules: closing first just keeps things more graceful - closeMotorControllers(); - closeServoControllers(); - // Now close everything that's USB-connected (yes that might re-close a motor or servo - // controller, but that's ok - closeAutoCloseOnTeardown(); + // shutdown everything we have open + closeHardwareMap(hardwareMap); + closeHardwareMap(hardwareMapExtra); // Stop the battery monitoring so we don't send stale telemetry closeBatteryMonitoring(); @@ -136,6 +147,19 @@ public void close() { eventLoopManager = null; } + protected static void closeHardwareMap(HardwareMap hardwareMap) { + + // Close motor and servo controllers first, since some of them may reside on top + // of legacy modules: closing first just keeps things more graceful + closeMotorControllers(hardwareMap); + closeServoControllers(hardwareMap); + + // Now close everything that's USB-connected (yes that might re-close a motor or servo + // controller, but that's ok + closeAutoCloseOnTeardown(hardwareMap); + } + + //------------------------------------------------------------------------------------------------ // Accessing //------------------------------------------------------------------------------------------------ @@ -150,6 +174,7 @@ public HardwareMap getHardwareMap() throws RobotCoreException, InterruptedExcept // Create a newly-active hardware map hardwareMap = hardwareFactory.createHardwareMap(eventLoopManager); + hardwareMapExtra = new HardwareMap(robotControllerContext); } return hardwareMap; } @@ -158,14 +183,104 @@ public HardwareMap getHardwareMap() throws RobotCoreException, InterruptedExcept public List getExtantLynxDeviceImpls() { synchronized (hardwareFactory) { List result = new ArrayList(); - HardwareMap map = hardwareMap; - if (map != null) { - for (LynxUsbDevice lynxUsbDevice : map.getAll(LynxUsbDevice.class)) { + if (hardwareMap != null) { + for (LynxUsbDevice lynxUsbDevice : hardwareMap.getAll(LynxUsbDevice.class)) { if (lynxUsbDevice.getArmingState()==RobotArmingStateNotifier.ARMINGSTATE.ARMED) { result.add(lynxUsbDevice.getDelegationTarget()); } } } + if (hardwareMapExtra != null) { + for (LynxUsbDevice lynxUsbDevice : hardwareMapExtra.getAll(LynxUsbDevice.class)) { + if (lynxUsbDevice.getArmingState()==RobotArmingStateNotifier.ARMINGSTATE.ARMED) { + result.add(lynxUsbDevice.getDelegationTarget()); + } + } + } + return result; + } + } + + /** + * Returns the device whose serial number is the one indicated, from the hardware map if possible + * but instantiating / opening it if necessary. null is returned if the object cannot be + * accessed. + * + * @param classOrInterface the interface to retrieve on the returned object + * @param serialNumber the serial number of the object to retrieve + * @param usbScanManagerSupplier how to get a {@link USBScanManager} if it ends up we need one + */ + public @Nullable T getHardwareDevice(Class classOrInterface, final SerialNumber serialNumber, Supplier usbScanManagerSupplier) { + synchronized (hardwareFactory) { + RobotLog.vv(TAG, "getHardwareDevice(%s)...", serialNumber); + try { + getHardwareMap(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return null; + } catch (RobotCoreException e) { + return null; + } + + Object oResult = hardwareMap.get(Object.class, serialNumber); + + if (oResult == null) { + oResult = hardwareMapExtra.get(Object.class, serialNumber); + } + + if (oResult == null) { + /** the device isn't in the hwmap. but is it actually attached? */ + /** first, check for it's scannable */ + final SerialNumber scannableSerialNumber = serialNumber.getScannableDeviceSerialNumber(); + + boolean tryScannable = true; + if (!scannableSerialNumber.equals(serialNumber)) { // already did that check + if (hardwareMap.get(Object.class, scannableSerialNumber) != null || hardwareMapExtra.get(Object.class, scannableSerialNumber) != null) { + RobotLog.ee(TAG, "internal error: %s absent but scannable %s present", serialNumber, scannableSerialNumber); + tryScannable = false; + } + } + + if (tryScannable) { + final USBScanManager usbScanManager = usbScanManagerSupplier.get(); + if (usbScanManager != null) { + try { + ScannedDevices scannedDevices = usbScanManager.awaitScannedDevices(); + if (scannedDevices.containsKey(scannableSerialNumber)) { + /** yes, it's there. build a new configuration for it */ + ConfigurationUtility configurationUtility = new ConfigurationUtility(); + ControllerConfiguration controllerConfiguration = configurationUtility.buildNewControllerConfiguration(scannableSerialNumber, scannedDevices.get(scannableSerialNumber), usbScanManager.getLynxModuleMetaListSupplier(scannableSerialNumber)); + if (controllerConfiguration != null) { + controllerConfiguration.setEnabled(true); + controllerConfiguration.setKnownToBeAttached(true); + /** get access to the actual device */ + hardwareFactory.instantiateConfiguration(hardwareMapExtra, controllerConfiguration, eventLoopManager); + oResult = hardwareMapExtra.get(Object.class, serialNumber); + RobotLog.ii(TAG, "found %s: hardwareMapExtra:", serialNumber); + hardwareMapExtra.logDevices(); + } else { + RobotLog.ee(TAG, "buildNewControllerConfiguration(%s) failed", scannableSerialNumber); + } + } else { + RobotLog.ee(TAG, ""); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (RobotCoreException e) { + RobotLog.ee(TAG, e, "exception in getHardwareDevice(%s)", serialNumber); + } + } else { + RobotLog.ee(TAG, "usbScanManager supplied as null"); + } + } + } + + T result = null; + if (oResult != null && classOrInterface.isInstance(oResult)) { + result = classOrInterface.cast(oResult); + } + + RobotLog.vv(TAG, "...getHardwareDevice(%s)=%s,%s", serialNumber, oResult, result); return result; } } @@ -242,7 +357,7 @@ public void refreshUserTelemetry(TelemetryMessage telemetry, double requestedInt if (transmitBecauseOfBattery) { telemetry.addData(EventLoopManager.ROBOT_BATTERY_LEVEL_KEY, buildRobotBatteryMsg()); robotBatteryTimer.reset(); - if (robotBatteryLoggingTimer.seconds() > robotBatteryLoggingInterval) { + if ((DEBUG) && (robotBatteryLoggingTimer.seconds() > robotBatteryLoggingInterval)) { RobotLog.i("robot battery read duration: n=%d, mean=%.3fms sd=%.3fms", robotBatteryStatistics.getCount(), robotBatteryStatistics.getMean(), robotBatteryStatistics.getStandardDeviation()); robotBatteryLoggingTimer.reset(); } @@ -334,21 +449,27 @@ public void sendTelemetry(String tag, String msg) { telemetry.clearData(); } - protected void closeMotorControllers() { - for (DcMotorController controller : hardwareMap.getAll(DcMotorController.class)) { - controller.close(); + protected static void closeMotorControllers(HardwareMap hardwareMap) { + if (hardwareMap != null) { + for (DcMotorController controller : hardwareMap.getAll(DcMotorController.class)) { + controller.close(); + } } } - protected void closeServoControllers() { - for (ServoController controller : hardwareMap.getAll(ServoController.class)) { - controller.close(); + protected static void closeServoControllers(HardwareMap hardwareMap) { + if (hardwareMap != null) { + for (ServoController controller : hardwareMap.getAll(ServoController.class)) { + controller.close(); + } } } - protected void closeAutoCloseOnTeardown() { - for (HardwareDeviceCloseOnTearDown device : hardwareMap.getAll(HardwareDeviceCloseOnTearDown.class)) { - device.close(); + protected static void closeAutoCloseOnTeardown(HardwareMap hardwareMap) { + if (hardwareMap != null) { + for (HardwareDeviceCloseOnTearDown device : hardwareMap.getAll(HardwareDeviceCloseOnTearDown.class)) { + device.close(); + } } } diff --git a/library/src/main/java/com/qualcomm/ftccommon/FtcLynxFirmwareUpdateActivity.java b/library/src/main/java/com/qualcomm/ftccommon/FtcLynxFirmwareUpdateActivity.java index fa417e1..a4cd306 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/FtcLynxFirmwareUpdateActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/FtcLynxFirmwareUpdateActivity.java @@ -33,8 +33,14 @@ are permitted (subject to the limitations in the disclaimer below) provided that package com.qualcomm.ftccommon; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; import android.widget.Button; +import android.widget.ListView; import android.widget.TextView; import com.qualcomm.robotcore.exception.RobotCoreException; @@ -43,12 +49,15 @@ are permitted (subject to the limitations in the disclaimer below) provided that import com.qualcomm.robotcore.util.RobotLog; import com.qualcomm.robotcore.util.ThreadPool; -import org.firstinspires.ftc.robotcore.internal.system.AppUtil; +import org.firstinspires.ftc.robotcore.internal.collections.MutableReference; import org.firstinspires.ftc.robotcore.internal.collections.SimpleGson; import org.firstinspires.ftc.robotcore.internal.network.CallbackResult; import org.firstinspires.ftc.robotcore.internal.network.NetworkConnectionHandler; import org.firstinspires.ftc.robotcore.internal.network.RecvLoopRunnable; import org.firstinspires.ftc.robotcore.internal.network.RobotCoreCommandList.FWImage; +import org.firstinspires.ftc.robotcore.internal.stellaris.FlashLoaderManager; +import org.firstinspires.ftc.robotcore.internal.system.AppUtil; +import org.firstinspires.ftc.robotcore.internal.system.Deadline; import org.firstinspires.ftc.robotcore.internal.ui.ThemedActivity; import org.firstinspires.ftc.robotcore.internal.ui.UILocation; @@ -82,6 +91,7 @@ public class FtcLynxFirmwareUpdateActivity extends ThemedActivity protected FWImage firmwareImageFile = new FWImage(new File(""), false); protected List modulesToUpdate = new ArrayList(); protected boolean enableUpdateButton = true; + protected boolean cancelUpdate = false; protected BlockingQueue availableLynxImages = new ArrayBlockingQueue(1); protected BlockingQueue availableLynxModules = new ArrayBlockingQueue(1); @@ -98,7 +108,7 @@ public static void initializeDirectories() String message = AppUtil.getDefContext().getString(R.string.lynxFirmwareUpdateReadme); ReadWriteFile.writeFile(AppUtil.LYNX_FIRMWARE_UPDATE_DIR, "readme.txt", message); - // We also here, out of conveience, do likewise for the RC app, even though that + // We also here, out of convenience, do likewise for the RC app, even though that // logically has nothing to do with firmware updating per se. message = AppUtil.getDefContext().getString(R.string.robotControllerAppUpdateReadme); ReadWriteFile.writeFile(AppUtil.RC_APP_UPDATE_DIR, "readme.txt", message); @@ -121,15 +131,19 @@ protected void onCreate(Bundle savedInstanceState) { super.onStart(); - TextView instructions = (TextView)findViewById(R.id.lynxFirmwareInstructions); - Button button = (Button)findViewById(R.id.lynxFirmwareUpdateButton); + TextView instructionsPre = (TextView)findViewById(R.id.lynxFirmwareInstructionsPre); + ListView modulesListView = (ListView)findViewById(R.id.lynxFirmwareModuleList); + TextView instructionsPost = (TextView)findViewById(R.id.lynxFirmwareInstructionsPost); + Button button = (Button)findViewById(R.id.lynxFirmwareUpdateButton); CommandList.LynxFirmwareImagesResp candidateImages = getCandidateLynxFirmwareImages(); if (candidateImages.firmwareImages.isEmpty()) { File relativePath = AppUtil.getInstance().getRelativePath(candidateImages.firstFolder.getParentFile(), AppUtil.LYNX_FIRMWARE_UPDATE_DIR); - instructions.setText(getString(R.string.lynx_fw_instructions_no_binary, relativePath)); + instructionsPre.setText(getString(R.string.lynx_fw_instructions_no_binary, relativePath)); + modulesListView.setVisibility(View.GONE); + instructionsPost.setVisibility(View.GONE); button.setEnabled(false); } else @@ -147,19 +161,53 @@ protected void onCreate(Bundle savedInstanceState) modulesToUpdate = getLynxModulesForFirmwareUpdate(); if (modulesToUpdate.isEmpty()) { - instructions.setText(getString(R.string.lynx_fw_instructions_no_devices, firmwareImageFile.getName())); + instructionsPre.setText(getString(R.string.lynx_fw_instructions_no_devices, firmwareImageFile.getName())); + modulesListView.setVisibility(View.GONE); + instructionsPost.setVisibility(View.GONE); button.setEnabled(false); } else { - StringBuilder serials = new StringBuilder(); + instructionsPre.setText(getString(R.string.lynx_fw_instructions_update, firmwareImageFile.getName())); + + class Item + { + String title; + String banter; + Item(String title, String banter) { this.title = title; this.banter = banter; } + } + final List itemList = new ArrayList<>(); for (USBAccessibleLynxModule module : modulesToUpdate) { - serials.append(" "); - serials.append(module.getSerialNumber().toString()); - serials.append("\n"); + String serialNumber = getString(R.string.lynx_fw_instructions_serial, module.getSerialNumber()); + String moduleAddress = module.getModuleAddress()==0 ? getString(R.string.lynx_fw_instructions_module_address_unavailable) : getString(R.string.lynx_fw_instructions_module_address, module.getModuleAddress()); + String firmware = getString(R.string.lynx_fw_instructions_firmware_version, module.getFinishedFirmwareVersionString()); + String description = serialNumber + "\n" + + moduleAddress + "\n" + + firmware; + itemList.add(new Item(AppUtil.getDefContext().getString(R.string.lynx_fw_instructions_item_title), description)); } - instructions.setText(getString(R.string.lynx_fw_instructions_update, firmwareImageFile.getName(), serials.toString())); + final int layoutRes = android.R.layout.simple_list_item_2; + modulesListView.setAdapter(new ArrayAdapter(this, layoutRes, /*eh?*/ android.R.id.text1, itemList) + { + @Override public @NonNull View getView(int position, @Nullable View view, @NonNull ViewGroup parent) + { + if (view == null) + { + view = LayoutInflater.from(getContext()).inflate(layoutRes, parent, false); + } + View itemView = super.getView(position, view, parent); + TextView topLine = (TextView) itemView.findViewById(android.R.id.text1); + TextView bottomLine = (TextView) itemView.findViewById(android.R.id.text2); + + Item item = getItem(position); + topLine.setVisibility(View.GONE); + bottomLine.setText(item.banter); + + return view; + } + }); + button.setEnabled(enableUpdateButton); } } @@ -175,6 +223,13 @@ protected void onCreate(Bundle savedInstanceState) // UI interaction //---------------------------------------------------------------------------------------------- + protected enum FwResponseStatus + { + Succeeded, + TimedOut, + Cancelled + } + public void onUpdateLynxFirmwareClicked(View view) { // A second push is meaningless once we've entered fw update mode once @@ -184,6 +239,10 @@ public void onUpdateLynxFirmwareClicked(View view) // We're running on the UI thread here (of course). And the execution of the command // will need to put up dialogs, which means we can't be stuck here inside this method while // we do that. So we do the updating in a worker. + // + // We at least dismiss our background work here if the user dismisses our activity. + // But we leave any actual update running to completion. That's probably a good thing, + // (avoid bricking) but we might do better in managing the UI. Tricky back-and-forth, though. ThreadPool.getDefault().execute(new Runnable() { @@ -191,6 +250,9 @@ public void onUpdateLynxFirmwareClicked(View view) { for (USBAccessibleLynxModule module : modulesToUpdate) { + if (cancelUpdate) + break; + availableFWUpdateResps.clear(); RobotLog.vv(TAG, "updating %s with %s", module.getSerialNumber(), firmwareImageFile.getName()); @@ -199,16 +261,19 @@ public void onUpdateLynxFirmwareClicked(View view) params.firmwareImageFile = firmwareImageFile; sendOrInject(new Command(CommandList.CMD_LYNX_FIRMWARE_UPDATE, SimpleGson.getInstance().toJson(params))); - CommandList.LynxFirmwareUpdateResp respParams = awaitResponse(availableFWUpdateResps, null, 30, TimeUnit.SECONDS); + MutableReference status = new MutableReference<>(FwResponseStatus.Succeeded); + CommandList.LynxFirmwareUpdateResp respParams = awaitResponse(availableFWUpdateResps, null, FlashLoaderManager.secondsFirmwareUpdateTimeout, TimeUnit.SECONDS, status); if (respParams != null && respParams.success) { String message = getString(R.string.toastLynxFirmwareUpdateSuccessful, module.getSerialNumber()); RobotLog.vv(TAG, "%s", message); AppUtil.getInstance().showToast(UILocation.BOTH, message); } - else + else if (status.getValue() != FwResponseStatus.Cancelled) { - String message = getString(R.string.alertLynxFirmwareUpdateFailed, module.getSerialNumber()); + String message = respParams==null + ? getString(R.string.alertLynxFirmwareUpdateTimedout, module.getSerialNumber()) + : getString(R.string.alertLynxFirmwareUpdateFailed, module.getSerialNumber()); RobotLog.ee(TAG, "%s", message); AppUtil.DialogContext alertDialogContext = AppUtil.getInstance().showAlertDialog(UILocation.BOTH, getString(R.string.alertLynxFirmwareUpdateFailedTitle), message); try { @@ -227,6 +292,12 @@ public void onUpdateLynxFirmwareClicked(View view) }); } + @Override protected void onPause() + { + super.onPause(); + cancelUpdate = true; + } + @Override protected void onStop() { super.onStop(); @@ -285,7 +356,7 @@ protected List getLynxModulesForFirmwareUpdate() // Send the command availableLynxModules.clear(); - request.includeModuleNumbers = false; + request.forFirmwareUpdate = true; sendOrInject(new Command(CommandList.CMD_GET_USB_ACCESSIBLE_LYNX_MODULES, request.serialize())); // Wait, but only a while, for the result @@ -309,16 +380,27 @@ protected void sendOrInject(Command cmd) protected T awaitResponse(BlockingQueue queue, T defaultResponse) { - return awaitResponse(queue, defaultResponse, msResponseWait, TimeUnit.MILLISECONDS); + return awaitResponse(queue, defaultResponse, msResponseWait, TimeUnit.MILLISECONDS, new MutableReference(FwResponseStatus.Succeeded)); } - protected T awaitResponse(BlockingQueue queue, T defaultResponse, long time, TimeUnit timeUnit) + protected T awaitResponse(BlockingQueue queue, T defaultResponse, long time, TimeUnit timeUnit, MutableReference status) { try { - T cur = queue.poll(time, timeUnit); - if (cur != null) + Deadline deadline = new Deadline(time, timeUnit); + status.setValue(FwResponseStatus.TimedOut); + while (!deadline.hasExpired()) { - return cur; + T cur = queue.poll(100, TimeUnit.MILLISECONDS); + if (cur != null) + { + status.setValue(FwResponseStatus.Succeeded); + return cur; + } + if (cancelUpdate) + { + status.setValue(FwResponseStatus.Cancelled); + break; + } } } catch (InterruptedException e) diff --git a/library/src/main/java/com/qualcomm/ftccommon/FtcLynxModuleAddressUpdateActivity.java b/library/src/main/java/com/qualcomm/ftccommon/FtcLynxModuleAddressUpdateActivity.java index b5e788a..870657b 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/FtcLynxModuleAddressUpdateActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/FtcLynxModuleAddressUpdateActivity.java @@ -47,6 +47,7 @@ are permitted (subject to the limitations in the disclaimer below) provided that import com.qualcomm.ftccommon.configuration.EditActivity; import com.qualcomm.robotcore.exception.RobotCoreException; +import com.qualcomm.robotcore.hardware.configuration.LynxConstants; import com.qualcomm.robotcore.robocol.Command; import com.qualcomm.robotcore.util.RobotLog; import com.qualcomm.robotcore.util.SerialNumber; @@ -148,7 +149,7 @@ protected void onStart() protected class DisplayedModuleList { - protected int lastModuleAddressChoice = 10; + protected int lastModuleAddressChoice = LynxConstants.MAX_MODULE_ADDRESS_CHOICE; protected AddressConfiguration currentAddressConfiguration = new AddressConfiguration(); protected ViewGroup moduleList; @@ -547,7 +548,7 @@ protected List getUSBAccessibleLynxModules() // Send the command availableLynxModules.clear(); - request.includeModuleNumbers = true; + request.forFirmwareUpdate = true; sendOrInject(new Command(CommandList.CMD_GET_USB_ACCESSIBLE_LYNX_MODULES, request.serialize())); // Wait, but only a while, for the result @@ -557,18 +558,6 @@ protected List getUSBAccessibleLynxModules() return result.modules; } - protected void sendOrInject(Command cmd) - { - if (remoteConfigure) - { - NetworkConnectionHandler.getInstance().sendCommand(cmd); - } - else - { - NetworkConnectionHandler.getInstance().injectReceivedCommand(cmd); - } - } - protected T awaitResponse(BlockingQueue queue, T defaultResponse) { return awaitResponse(queue, defaultResponse, msResponseWait, TimeUnit.MILLISECONDS); diff --git a/library/src/main/java/com/qualcomm/ftccommon/FtcRobotControllerService.java b/library/src/main/java/com/qualcomm/ftccommon/FtcRobotControllerService.java index 061df3e..d7cbf65 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/FtcRobotControllerService.java +++ b/library/src/main/java/com/qualcomm/ftccommon/FtcRobotControllerService.java @@ -34,12 +34,12 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import android.app.Service; import android.content.Context; import android.content.Intent; +import android.graphics.Color; import android.os.Binder; -import android.os.Build; import android.os.IBinder; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; -import com.qualcomm.hardware.lynx.LynxModule; import com.qualcomm.robotcore.eventloop.EventLoop; import com.qualcomm.robotcore.eventloop.EventLoopManager; import com.qualcomm.robotcore.eventloop.opmode.EventLoopManagerClient; @@ -49,8 +49,8 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.qualcomm.robotcore.hardware.LightBlinker; import com.qualcomm.robotcore.hardware.LightMultiplexor; import com.qualcomm.robotcore.hardware.SwitchableLight; +import com.qualcomm.robotcore.hardware.configuration.ConfigurationTypeManager; import com.qualcomm.robotcore.hardware.configuration.LynxConstants; -import com.qualcomm.robotcore.hardware.configuration.UserConfigurationTypeManager; import com.qualcomm.robotcore.robot.Robot; import com.qualcomm.robotcore.robot.RobotState; import com.qualcomm.robotcore.robot.RobotStatus; @@ -60,16 +60,17 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.qualcomm.robotcore.wifi.NetworkConnection; import com.qualcomm.robotcore.wifi.NetworkConnectionFactory; import com.qualcomm.robotcore.wifi.NetworkType; -import com.qualcomm.robotcore.wifi.WifiDirectAssistant; import org.firstinspires.ftc.robotcore.internal.hardware.DragonboardIndicatorLED; import org.firstinspires.ftc.robotcore.internal.network.CallbackResult; +import org.firstinspires.ftc.robotcore.internal.network.NetworkConnectionHandler; import org.firstinspires.ftc.robotcore.internal.network.PeerStatus; import org.firstinspires.ftc.robotcore.internal.network.PreferenceRemoterRC; import org.firstinspires.ftc.robotcore.internal.network.WifiDirectAgent; import org.firstinspires.ftc.robotcore.internal.system.PreferencesHelper; import org.firstinspires.ftc.robotcore.internal.webserver.WebServer; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -94,7 +95,7 @@ public class FtcRobotControllerService extends Service implements NetworkConnect private EventLoop eventLoop; private EventLoop idleEventLoop; - private WifiDirectAssistant.Event networkConnectionStatus = WifiDirectAssistant.Event.UNKNOWN; + private NetworkConnection.NetworkEvent networkConnectionStatus = NetworkConnection.NetworkEvent.UNKNOWN; private RobotStatus robotStatus = RobotStatus.NONE; private PeerStatus peerStatus = PeerStatus.DISCONNECTED; @@ -108,7 +109,7 @@ public class FtcRobotControllerService extends Service implements NetworkConnect private WifiDirectAgent wifiDirectAgent = WifiDirectAgent.getInstance(); private final Object wifiDirectCallbackLock = new Object(); - private final WebServer webServer = new WebServer(); + private WebServer webServer; //---------------------------------------------------------------------------------------------- // Initialization @@ -151,8 +152,8 @@ private void updatePeerStatus(PeerStatus peerStatus) { FtcRobotControllerService.this.peerStatus = peerStatus; // When we connect, send useful info to the driver station if (peerStatus == PeerStatus.CONNECTED) { - UserConfigurationTypeManager.getInstance().sendUserDeviceTypes(); - PreferenceRemoterRC.getInstance().sendAllPreferences(); + ConfigurationTypeManager.getInstance().sendUserDeviceTypes(); + PreferenceRemoterRC.getInstance().sendAllPreferences(); // TODO: We should probably also do this periodically later in case this initial version didn't get through } } // Do the UI stuff as well @@ -170,6 +171,12 @@ public void onTelemetryTransmitted() { * getting the robot ready to run. If interrupted, it should return quickly and promptly. */ private class RobotSetupRunnable implements Runnable { + @Nullable Runnable runOnComplete; + + RobotSetupRunnable(@Nullable Runnable runOnComplete) { + this.runOnComplete = runOnComplete; + } + //---------------------------------------------------------------------------------------------- // Building blocks //---------------------------------------------------------------------------------------------- @@ -230,6 +237,7 @@ boolean waitForWifiDirect() throws InterruptedException { } boolean waitForNetworkConnection() throws InterruptedException { + RobotLog.vv(TAG, "Waiting for a connection to a wifi service"); updateRobotStatus(RobotStatus.WAITING_ON_NETWORK_CONNECTION); boolean waited = false; for (;;) { @@ -240,17 +248,17 @@ boolean waitForNetworkConnection() throws InterruptedException { } void waitForNetwork() throws InterruptedException { - waitForWifi(); if (networkConnection.getNetworkType() == NetworkType.WIFIDIRECT) { + waitForWifi(); waitForWifiDirect(); // Re-issue createConnection(): we might have just brought up the network in one of the // waits, so the previous createConnection() in bind above might have failed. networkConnection.createConnection(); - - // Wait until we're free and clear to go - waitForNetworkConnection(); } + // Wait until we're free and clear to go + waitForNetworkConnection(); + webServer.start(); } void startRobot() throws RobotCoreException { @@ -280,6 +288,10 @@ void startRobot() throws RobotCoreException { RobotLog.setGlobalErrorMsg(e, getString(R.string.globalErrorFailedToCreateRobot)); } catch (InterruptedException e) { updateRobotStatus(RobotStatus.ABORT_DUE_TO_INTERRUPT); + } finally { + if (runOnComplete != null) { + runOnComplete.run(); + } } }}); @@ -310,7 +322,7 @@ public NetworkConnection getNetworkConnection() { return networkConnection; } - public NetworkConnection.Event getNetworkConnectionStatus() { + public NetworkConnection.NetworkEvent getNetworkConnectionStatus() { return networkConnectionStatus; } @@ -341,16 +353,24 @@ public Robot getRobot() { @Override public IBinder onBind(Intent intent) { RobotLog.vv(TAG, "onBind()"); - RobotLog.vv(TAG, "Android Device: maker=%s model=%s sdk=%d", Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT); preferencesHelper.writeBooleanPrefIfDifferent(getString(R.string.pref_wifip2p_remote_channel_change_works), Device.wifiP2pRemoteChannelChangeWorks()); preferencesHelper.writeBooleanPrefIfDifferent(getString(R.string.pref_has_independent_phone_battery), !LynxConstants.isRevControlHub()); + boolean hasSpeaker = !LynxConstants.isRevControlHub(); + preferencesHelper.writeBooleanPrefIfDifferent(getString(R.string.pref_has_speaker), hasSpeaker); + if (!hasSpeaker) { + /** Turn off the sound if no speaker (helps UI; see {@link FtcRobotControllerSettingsActivity} */ + preferencesHelper.writeBooleanPrefIfDifferent(getString(R.string.pref_sound_on_off), false); + } FtcLynxFirmwareUpdateActivity.initializeDirectories(); NetworkType networkType = (NetworkType) intent.getSerializableExtra(NetworkConnectionFactory.NETWORK_CONNECTION_TYPE); - networkConnection = NetworkConnectionFactory.getNetworkConnection(networkType, getBaseContext()); - networkConnection.setCallback(this); + webServer = new WebServer(networkType); + + NetworkConnectionHandler networkConnectionHandler = NetworkConnectionHandler.getInstance(); + networkConnectionHandler.pushNetworkConnectionCallback(this); + networkConnection = NetworkConnectionFactory.getNetworkConnection(networkType, getBaseContext()); networkConnection.enable(); networkConnection.createConnection(); @@ -397,7 +417,11 @@ protected void startLEDS() { }, 10, TimeUnit.SECONDS); // livenessIndicatorBlinker = new LightBlinker(LightMultiplexor.forLight(DragonboardIndicatorLED.forIndex(LynxConstants.INDICATOR_LED_ROBOT_CONTROLLER_ALIVE))); - List steps = LynxModule.getLivenessSteps(); + int msLivenessLong = 5000; + int msLivenessShort = 500; + List steps = new ArrayList<>(); + steps.add(new Blinker.Step(Color.GREEN, msLivenessLong - msLivenessShort, TimeUnit.MILLISECONDS)); + steps.add(new Blinker.Step(Color.BLACK, msLivenessShort, TimeUnit.MILLISECONDS)); livenessIndicatorBlinker.setPattern(steps); } } @@ -421,7 +445,7 @@ public synchronized void setCallback(UpdateUI.Callback callback) { this.callback = callback; } - public synchronized void setupRobot(EventLoop eventLoop, EventLoop idleEventLoop) { + public synchronized void setupRobot(EventLoop eventLoop, EventLoop idleEventLoop, @Nullable Runnable runOnComplete) { /* * (Possibly out-of-date comment:) @@ -434,13 +458,10 @@ public synchronized void setupRobot(EventLoop eventLoop, EventLoop idleEventLoop shutdownRobotSetup(); - RobotLog.clearGlobalErrorMsg(); - RobotLog.clearGlobalWarningMsg(); - this.eventLoop = eventLoop; this.idleEventLoop = idleEventLoop; - robotSetupFuture = ThreadPool.getDefault().submit(new RobotSetupRunnable()); + robotSetupFuture = ThreadPool.getDefault().submit(new RobotSetupRunnable(runOnComplete)); } void shutdownRobotSetup() { @@ -461,7 +482,7 @@ public synchronized void shutdownRobot() { } @Override - public CallbackResult onNetworkConnectionEvent(NetworkConnection.Event event) { + public CallbackResult onNetworkConnectionEvent(NetworkConnection.NetworkEvent event) { CallbackResult result = CallbackResult.NOT_HANDLED; switch (event) { case CONNECTED_AS_GROUP_OWNER: @@ -479,6 +500,10 @@ public CallbackResult onNetworkConnectionEvent(NetworkConnection.Event event) { break; case CONNECTION_INFO_AVAILABLE: RobotLog.ii(TAG, "Network Connection Passphrase: " + networkConnection.getPassphrase()); + // Handling the case where we are changing networks and the web server has already been started. + if (webServer.wasStarted()) { + webServer.stop(); + } webServer.start(); break; case ERROR: @@ -494,7 +519,7 @@ public CallbackResult onNetworkConnectionEvent(NetworkConnection.Event event) { return result; } - private void updateNetworkConnectionStatus(final WifiDirectAssistant.Event event) { + private void updateNetworkConnectionStatus(final NetworkConnection.NetworkEvent event) { networkConnectionStatus = event; if (callback != null) callback.networkConnectionUpdate(networkConnectionStatus); } diff --git a/library/src/main/java/com/qualcomm/ftccommon/FtcRobotControllerSettingsActivity.java b/library/src/main/java/com/qualcomm/ftccommon/FtcRobotControllerSettingsActivity.java index 37c828c..7679ea1 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/FtcRobotControllerSettingsActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/FtcRobotControllerSettingsActivity.java @@ -35,10 +35,15 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import android.os.Bundle; import android.preference.Preference; import android.preference.PreferenceFragment; +import android.preference.SwitchPreference; +import android.widget.Switch; import com.qualcomm.robotcore.util.RobotLog; +import org.firstinspires.ftc.robotcore.internal.network.DeviceNameManagerFactory; import org.firstinspires.ftc.robotcore.internal.system.AppUtil; +import org.firstinspires.ftc.robotcore.internal.network.WifiDirectDeviceNameManager; +import org.firstinspires.ftc.robotcore.internal.system.PreferencesHelper; import org.firstinspires.ftc.robotcore.internal.network.DeviceNameManager; import org.firstinspires.ftc.robotcore.internal.ui.ThemedActivity; @@ -63,6 +68,13 @@ public void onCreate(Bundle savedInstanceState) { return true; } }); + + PreferencesHelper preferencesHelper = new PreferencesHelper(getTag()); + if (!preferencesHelper.readBoolean(getString(R.string.pref_has_speaker), true)) { + // Disable turning on and off sound if there's no speaker + Preference prefSoundOnOff = findPreference(getString(R.string.pref_sound_on_off)); + prefSoundOnOff.setEnabled(false); + } } @Override @@ -82,7 +94,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Always make sure we have a real device name before we launch - DeviceNameManager.getInstance().initializeDeviceNameIfNecessary(); + DeviceNameManagerFactory.getInstance().initializeDeviceNameIfNecessary(); // Display the fragment as the main content. getFragmentManager().beginTransaction() diff --git a/library/src/main/java/com/qualcomm/ftccommon/FtcWifiDirectChannelSelectorActivity.java b/library/src/main/java/com/qualcomm/ftccommon/FtcWifiDirectChannelSelectorActivity.java index 7cd2318..7c5e473 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/FtcWifiDirectChannelSelectorActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/FtcWifiDirectChannelSelectorActivity.java @@ -62,9 +62,11 @@ are permitted (subject to the limitations in the disclaimer below) provided that package com.qualcomm.ftccommon; +import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.net.wifi.WifiManager; +import android.os.Build; import android.os.Bundle; import android.support.annotation.AnyRes; import android.support.annotation.LayoutRes; @@ -80,9 +82,9 @@ are permitted (subject to the limitations in the disclaimer below) provided that import com.qualcomm.robotcore.util.RobotLog; -import org.firstinspires.ftc.robotcore.internal.system.AppUtil; import org.firstinspires.ftc.robotcore.internal.network.WifiDirectChannelAndDescription; import org.firstinspires.ftc.robotcore.internal.network.WifiDirectChannelChanger; +import org.firstinspires.ftc.robotcore.internal.system.AppUtil; import org.firstinspires.ftc.robotcore.internal.system.PreferencesHelper; import org.firstinspires.ftc.robotcore.internal.ui.ThemedActivity; import org.firstinspires.ftc.robotcore.internal.ui.UILocation; @@ -149,6 +151,18 @@ protected void loadAdapter(ListView itemsListView) { WifiDirectChannelAndDescription[] items = WifiDirectChannelAndDescription.load().toArray(new WifiDirectChannelAndDescription[0]); Arrays.sort(items); + + // if 5GHz is not available, then truncate list of available channels. + if (is5GHzAvailable() == false) + { + items = Arrays.copyOf(items, INDEX_AUTO_AND_2_4_ITEMS); + RobotLog.vv(TAG, "5GHz radio not available."); + } + else + { + RobotLog.vv(TAG, "5GHz radio is available."); + } + ArrayAdapter adapter = new WifiChannelItemAdapter(this, android.R.layout.simple_spinner_dropdown_item, items); // simple_spinner_item, simple_spinner_dropdown_item itemsListView.setAdapter(adapter); } @@ -217,4 +231,26 @@ public void onWifiSettingsClicked(View view) RobotLog.vv(TAG, "launch wifi settings"); startActivity(new Intent(WifiManager.ACTION_PICK_WIFI_NETWORK)); } + + + //---------------------------------------------------------------------------------------------- + // Additional variables and methods + //---------------------------------------------------------------------------------------------- + private final int INDEX_AUTO_AND_2_4_ITEMS = 12; + + private boolean is5GHzAvailable() + { + if (android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) + { + // it's a kit kat device or lower. + // assume 5GHz is not available; + return false; + } + else + { + WifiManager wifiManager = (WifiManager) this.getApplicationContext().getSystemService(WIFI_SERVICE); + return wifiManager.is5GHzBandSupported(); + } + } + } diff --git a/library/src/main/java/com/qualcomm/ftccommon/LaunchActivityConstantsList.java b/library/src/main/java/com/qualcomm/ftccommon/LaunchActivityConstantsList.java index d611b74..baeae48 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/LaunchActivityConstantsList.java +++ b/library/src/main/java/com/qualcomm/ftccommon/LaunchActivityConstantsList.java @@ -39,7 +39,6 @@ public class LaunchActivityConstantsList { public static final String ZTE_WIFI_CHANNEL_EDITOR_PACKAGE = "com.zte.wifichanneleditor"; public static final String VIEW_LOGS_ACTIVITY_FILENAME = "org.firstinspires.ftc.ftccommon.logFilename"; - public static final String ABOUT_ACTIVITY_CONNECTION_TYPE = "org.firstinspires.ftc.ftccommon.connectionType"; // Related to programming mode. /** diff --git a/library/src/main/java/com/qualcomm/ftccommon/SoundPlayer.java b/library/src/main/java/com/qualcomm/ftccommon/SoundPlayer.java index 52daa5d..ca388da 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/SoundPlayer.java +++ b/library/src/main/java/com/qualcomm/ftccommon/SoundPlayer.java @@ -37,52 +37,109 @@ are permitted (subject to the limitations in the disclaimer below) provided that import android.media.AudioManager; import android.media.MediaPlayer; import android.media.SoundPool; -import android.os.Looper; +import android.net.Uri; import android.preference.PreferenceManager; +import android.support.annotation.CheckResult; +import android.support.annotation.Nullable; import android.support.annotation.RawRes; -import com.qualcomm.robotcore.util.ElapsedTime; +import com.qualcomm.robotcore.robocol.Command; import com.qualcomm.robotcore.util.RobotLog; import com.qualcomm.robotcore.util.ThreadPool; +import com.qualcomm.robotcore.util.TypeConversion; +import org.firstinspires.ftc.robotcore.external.function.Consumer; +import org.firstinspires.ftc.robotcore.internal.android.SoundPoolIntf; +import org.firstinspires.ftc.robotcore.internal.collections.MutableReference; +import org.firstinspires.ftc.robotcore.internal.files.FileBasedLock; +import org.firstinspires.ftc.robotcore.internal.network.CallbackLooper; +import org.firstinspires.ftc.robotcore.internal.network.CallbackResult; +import org.firstinspires.ftc.robotcore.internal.network.NetworkConnectionHandler; import org.firstinspires.ftc.robotcore.internal.system.AppUtil; +import org.firstinspires.ftc.robotcore.internal.system.Deadline; +import org.firstinspires.ftc.robotcore.internal.system.Misc; +import org.firstinspires.ftc.robotcore.internal.system.RefCounted; +import org.firstinspires.ftc.robotcore.internal.system.Tracer; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.Flushable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * {@link SoundPlayer} is a simple utility class that plays sounds on the phone. The class - * is typically used through its singleton instance. + * is used through its singleton instance. * - * @see SoundPlayer#play(Context, int) + * @see SoundPlayer#startPlaying */ -@SuppressWarnings("javadoc") -public class SoundPlayer implements SoundPool.OnLoadCompleteListener +@SuppressWarnings({"javadoc", "WeakerAccess"}) +public class SoundPlayer implements SoundPool.OnLoadCompleteListener, SoundPoolIntf { //---------------------------------------------------------------------------------------------- // State //---------------------------------------------------------------------------------------------- public static final String TAG = "SoundPlayer"; - public static final boolean DEBUG = false; + public static boolean TRACE = true; + protected Tracer tracer = Tracer.create(TAG, TRACE); - protected static SoundPlayer theInstance = new SoundPlayer(3, 6); + protected static class InstanceHolder + { + public static SoundPlayer theInstance = new SoundPlayer(3, 6); // param choices are wet-finger-in-wind + } public static SoundPlayer getInstance() { - return theInstance; + return InstanceHolder.theInstance; } + public static final int msSoundTransmissionFreshness = 400; + + protected final Object lock = new Object(); + protected final boolean isRobotController = AppUtil.getInstance().isRobotController(); protected SoundPool soundPool; - protected @RawRes volatile int currentlyLoading; - protected long msFinishPlaying; + protected CountDownLatch currentlyLoadingLatch = null; + protected SoundInfo currentlyLoadingInfo = null; protected LoadedSoundCache loadedSounds; - protected ExecutorService executorService; - protected Looper looper; - protected Context context; + protected ExecutorService threadPool; + protected ScheduledExecutorService scheduledThreadPool; protected SharedPreferences sharedPreferences; - protected float soundOnLevel = 1.0f; - protected float soundOffLevel = 0.0f; + protected float soundOnVolume = 1.0f; + protected float soundOffVolume = 0.0f; + protected float masterVolume = 1.0f; + + protected static class CurrentlyPlaying + { + protected long msFinish = Long.MAX_VALUE; + protected int streamId = 0; + protected int loopControl = 0; + protected @Nullable Runnable runWhenFinished = null; + protected boolean isLooping() + { + return loopControl == -1; + } + } + protected Set currentlyPlayingSounds; + + protected enum StopWhat { All, Loops } //---------------------------------------------------------------------------------------------- // Construction @@ -100,164 +157,471 @@ public SoundPlayer(int simultaneousStreams, int cacheSize) { soundPool = new SoundPool(simultaneousStreams, AudioManager.STREAM_MUSIC, /*quality*/0); // can't use SoundPool.Builder on KitKat loadedSounds = new LoadedSoundCache(cacheSize); - currentlyLoading = 0; - msFinishPlaying = 0; - executorService = null; - looper = null; - context = AppUtil.getInstance().getApplication(); - sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + currentlyPlayingSounds = new HashSet<>(); + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(AppUtil.getDefContext()); // - startup(); + final CountDownLatch interlock = new CountDownLatch(1); + threadPool = ThreadPool.newFixedThreadPool(1, "SoundPlayer"); + scheduledThreadPool = ThreadPool.newScheduledExecutor(1, "SoundPlayerScheduler"); + CallbackLooper.getDefault().post(new Runnable() + { + @Override public void run() + { + // Must call setOnLoadCompleteListener() on a looper thread, the one on which + // we want to get completion callbacks to run. 'Bit of an odd API decision, but + // there you go! + soundPool.setOnLoadCompleteListener(SoundPlayer.this); + interlock.countDown(); + } + }); + try { interlock.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } + @Override public void close() { - shutdown(); + if (threadPool != null) + { + threadPool.shutdownNow(); + ThreadPool.awaitTerminationOrExitApplication(threadPool, 5, TimeUnit.SECONDS, "SoundPool", "internal error"); + threadPool = null; + } + if (scheduledThreadPool != null) + { + scheduledThreadPool.shutdownNow(); + ThreadPool.awaitTerminationOrExitApplication(scheduledThreadPool, 3, TimeUnit.SECONDS, "SoundPool", "internal error"); + } } - protected void startup() + /** + * Ensures that these local sounds are also in the local cache + */ + public void prefillSoundCache(@RawRes int... resourceIds) { - if (this.executorService == null) + for (@RawRes final int resId : resourceIds) { - this.executorService = ThreadPool.newFixedThreadPool(2, "SoundPlayer"); - - // Use one of those threads to run a Looper() that will ONLY see - // load completion callbacks from the SoundPool. - this.executorService.execute(new Runnable() + threadPool.submit(new Runnable() { @Override public void run() { - Thread.currentThread().setName("SoundPlayer looper"); - Looper.prepare(); - looper = Looper.myLooper(); - soundPool.setOnLoadCompleteListener(SoundPlayer.this); - Looper.loop(); // doesn't return until we tell it to quit() + ensureCached(AppUtil.getDefContext(), resId); } }); - - // Label the other thread - this.executorService.execute(new Runnable() - { - @Override public void run() - { - Thread.currentThread().setName("SoundPlayer"); - } - }); - - // Wait for us to learn the looper - while (looper==null) - { - Thread.yield(); - } } } - protected void shutdown() + //---------------------------------------------------------------------------------------------- + // Public API + //---------------------------------------------------------------------------------------------- + + public static class PlaySoundParams { - if (this.executorService != null) + /** an additional volume scaling that will be applied to this particular play action */ + public float volume = 1.0f; + + /** whether to wait for any currently-playing non-looping sound to finish before playing */ + public boolean waitForNonLoopingSoundsToFinish = true; + + /** -1 means playing loops forever, 0 is play once, 1 is play twice, etc */ + public int loopControl = 0; + + /** playback rate (1.0 = normal playback, range 0.5 to 2.0) */ + public float rate = 1.0f; + + //-------------- + + public PlaySoundParams() { } + + public PlaySoundParams(boolean wait) { this.waitForNonLoopingSoundsToFinish = wait; } + + public PlaySoundParams(PlaySoundParams them) { - if (looper != null) looper.quit(); - this.executorService.shutdownNow(); - ThreadPool.awaitTerminationOrExitApplication(this.executorService, 5, TimeUnit.SECONDS, "SoundPool", "internal error"); - this.executorService = null; - this.looper = null; + this.volume = them.volume; + this.waitForNonLoopingSoundsToFinish = them.waitForNonLoopingSoundsToFinish; + this.loopControl = them.loopControl; + this.rate = them.rate; } - } - //---------------------------------------------------------------------------------------------- - // Operations - //---------------------------------------------------------------------------------------------- + public boolean isLooping() + { + return loopControl == -1; + } + } /** * Asynchronously loads the indicated sound from its resource (if not already loaded), then - * initiates its play once any current sound is finished playing. + * initiates its play once any current non-looping sound is finished playing. * * @param context the context in which resId is to be interpreted * @param resId the resource id of the raw resource containing the sound. */ - synchronized public void play(final Context context, @RawRes final int resId) + public void startPlaying(final Context context, @RawRes final int resId) + { + startPlaying(context, resId, new PlaySoundParams(true), null,null); + } + public void startPlaying(final Context context, File file) { - play(context, resId, true); + startPlaying(context, file, new PlaySoundParams(true), null,null); } /** * Asynchronously loads the indicated sound from its resource (if not already loaded), then - * initiates its play, optionally waiting for any currently playing sound to finish first. + * initiates its play, optionally waiting for any currently non-looping playing sounds to finish first. * - * @param context the context in which resId is to be interpreted - * @param resId the resource id of the raw resource containing the sound. - * @param waitForCompletion whether to wait for any current sound to finish playing first or not + * @param context the context in which resId is to be interpreted + * @param resId the resource id of the raw resource containing the sound. + * @param params controls how the playback proceeds + * @param runWhenStarted executed when the stream starts to play + * @param runWhenFinished executed when the stream finishes playing */ - synchronized public void play(final Context context, @RawRes final int resId, final boolean waitForCompletion) + public void startPlaying(final Context context, @RawRes final int resId, final PlaySoundParams params, @Nullable final Consumer runWhenStarted, @Nullable final Runnable runWhenFinished) { - // Ignore impossible ids - if (resId==0) return; + threadPool.execute(new Runnable() + { + @Override public void run() + { + loadAndStartPlaying(context, resId, params, runWhenStarted, runWhenFinished); + } + }); + } - this.executorService.execute(new Runnable() + public void startPlaying(final Context context, final File file, final PlaySoundParams params, @Nullable final Consumer runWhenStarted, @Nullable final Runnable runWhenFinished) + { + if (file==null) return; + threadPool.execute(new Runnable() { @Override public void run() { - try { - loadAndPlay(context, resId, waitForCompletion); - } - catch (Exception e) + loadAndStartPlaying(context, file, params, runWhenStarted, runWhenFinished); + } + }); + } + + /** + * Stops playing all sounds that are currently playing + */ + @Override + public void stopPlayingAll() + { + internalStopPlaying(StopWhat.All); + } + + /** + * Stops playing all sounds that are currently playing in a loop + */ + public void stopPlayingLoops() + { + internalStopPlaying(StopWhat.Loops); + } + + protected void internalStopPlaying(StopWhat stopWhat) + { + synchronized (lock) + { + for (CurrentlyPlaying currentlyPlaying : currentlyPlayingSounds) + { + if (stopWhat==StopWhat.All || currentlyPlaying.isLooping() && stopWhat==StopWhat.Loops) { - RobotLog.ee(TAG, e, "exception playing sound; ignored"); + currentlyPlaying.msFinish = Long.MIN_VALUE; } } - }); + + checkForFinishedSounds(); + + if (isRobotController) + { + // Tell the driver station too + CommandList.CmdStopPlayingSounds cmdStopPlayingSounds = new CommandList.CmdStopPlayingSounds(stopWhat); + Command command = new Command(CommandList.CmdPlaySound.Command, cmdStopPlayingSounds.serialize()); + NetworkConnectionHandler.getInstance().sendCommand(command); + } + } } + /** - * Loads the requested sound if necessary, then (eventually) plays it. - * Note: this always runs on our dedicate executor thread. We do that because - * that allows us to have a looper that *only* sees load completions, which will - * prevent us from accepting new play requests while we're waiting for loads to - * complete. + * Preloads the sound so as to to reduce delays if the sound is subsequently played. */ - protected void loadAndPlay(Context context, @RawRes int resourceId, boolean waitForCompletion) + public boolean preload(Context context, @RawRes int resourceId) { - SoundInfo soundInfo = loadedSounds.get(resourceId); - if (soundInfo == null) + boolean result = false; + synchronized (lock) { - // Figure out how long the sound is. We do this before we load so - // as to avoid potential conflicts with the method of determining duration - int msDuration = getMsDuration(context, resourceId); + SoundInfo soundInfo = ensureLoaded(context, resourceId); + if (soundInfo != null) + { + result = true; + releaseRef(soundInfo); + } + } + return result; + } - // Ask to load the sound - currentlyLoading = resourceId; - try { + /** + * Preloads the sound so as to to reduce delays if the sound is subsequently played. + */ + @Override + public boolean preload(Context context, File file) + { + boolean result = false; + synchronized (lock) + { + SoundInfo soundInfo = ensureLoaded(context, file); + if (soundInfo != null) + { + result = true; + releaseRef(soundInfo); + } + } + return result; + } + + /** + * Sets the master volume control that is applied to all played sounds + * @see #getMasterVolume() + */ + public void setMasterVolume(float masterVolume) + { + synchronized (lock) + { + this.masterVolume = masterVolume; + } + } + + /** + * Returns the master volume control that is applied to all played sounds + * @see #setMasterVolume(float) + */ + public float getMasterVolume() + { + return this.masterVolume; + } + + + /** @deprecated use {@link #startPlaying(Context, int)} instead */ + @Deprecated + public void play(final Context context, @RawRes final int resId) + { + startPlaying(context, resId); + } + + /** @deprecated use {@link #startPlaying(Context, int, PlaySoundParams, Consumer, Runnable)} instead */ + @Deprecated + public void play(final Context context, @RawRes final int resId, final boolean waitForCompletion) + { + startPlaying(context, resId, new PlaySoundParams(waitForCompletion), null, null); + } + + //---------------------------------------------------------------------------------------------- + // Internal operations + //---------------------------------------------------------------------------------------------- + + protected void loadAndStartPlaying(Context context, @RawRes int resourceId, PlaySoundParams params, @Nullable final Consumer runWhenStarted, @Nullable Runnable runWhenFinished) + { + synchronized (lock) + { + SoundInfo soundInfo = ensureLoaded(context, resourceId); + if (soundInfo != null) + { + startPlayingLoadedSound(soundInfo, params, runWhenStarted, runWhenFinished); + releaseRef(soundInfo); + } + } + } + + protected void loadAndStartPlaying(Context context, File file, PlaySoundParams params, @Nullable final Consumer runWhenStarted, @Nullable Runnable runWhenFinished) + { + synchronized (lock) + { + SoundInfo soundInfo = ensureLoaded(context, file); + if (soundInfo != null) + { + startPlayingLoadedSound(soundInfo, params, runWhenStarted, runWhenFinished); + releaseRef(soundInfo); + } + } + } + + protected SoundInfo ensureLoaded(Context context, @RawRes int resourceId) // returns a ref + { + synchronized (lock) + { + SoundInfo result = loadedSounds.getResource(resourceId); + if (result == null) + { + int msDuration = getMsDuration(context, resourceId); + currentlyLoadingLatch = new CountDownLatch(1); + currentlyLoadingInfo = result = new SoundInfo(context, resourceId, msDuration); int sampleId = soundPool.load(context, resourceId, 1); if (sampleId != 0) { - // Remember the sound for next time - soundInfo = new SoundInfo(resourceId, sampleId, msDuration); - loadedSounds.put(resourceId, soundInfo); - - if (DEBUG) RobotLog.vv(TAG, "loadAndPlay(res=0x%08x samp=%d)...", resourceId, sampleId); + result.initialize(sampleId); + loadedSounds.putResource(resourceId, result); waitForLoadCompletion(); - if (DEBUG) RobotLog.vv(TAG, "...loaded"); - - // Play the sound - playLoadedSound(soundInfo, waitForCompletion); } else + tracer.traceError("unable to load sound resource 0x%08x", resourceId); + } + return result; + } + } + + protected SoundInfo ensureLoaded(Context context, File file) // returns a ref + { + synchronized (lock) + { + SoundInfo result = loadedSounds.getFile(file); + if (result == null) + { + int msDuration = getMsDuration(context, file); + currentlyLoadingLatch = new CountDownLatch(1); + currentlyLoadingInfo = result = new SoundInfo(file, msDuration); + int sampleId = soundPool.load(file.getAbsolutePath(), 1); + if (sampleId != 0) { - RobotLog.ee(TAG, "unable to load sound resource 0x%08x", resourceId); + result.initialize(sampleId); + loadedSounds.putFile(file, result); + waitForLoadCompletion(); } + else + tracer.traceError("unable to load sound %s", file); } - finally + return result; + } + } + + public boolean isLocalSoundOn() + { + return sharedPreferences.getBoolean(AppUtil.getDefContext().getString(R.string.pref_sound_on_off), true) + && sharedPreferences.getBoolean(AppUtil.getDefContext().getString(R.string.pref_has_speaker), true); + } + + void checkForFinishedSounds() + { + synchronized (lock) + { + long msNow = getMsNow(); + for (CurrentlyPlaying currentlyPlaying : new ArrayList<>(currentlyPlayingSounds)) // copy so we can remove while iterating { - currentlyLoading = 0; + if (currentlyPlaying.msFinish <= msNow) + { + soundPool.stop(currentlyPlaying.streamId); + if (currentlyPlaying.runWhenFinished != null) + { + threadPool.execute(currentlyPlaying.runWhenFinished); + } + currentlyPlayingSounds.remove(currentlyPlaying); + } } } - else + } + + // Play it for me, Sam. + protected void startPlayingLoadedSound(final SoundInfo soundInfo, @Nullable PlaySoundParams paramsIn, @Nullable final Consumer runWhenStarted, final @Nullable Runnable runWhenFinished) + { + // Get a writeable copy of the parameters + final PlaySoundParams params = paramsIn==null ? new PlaySoundParams() : new PlaySoundParams(paramsIn); + + // Scale the volume by the master + params.volume *= masterVolume; + + if (soundInfo != null) { - // Update the MRU notion of which sounds have been recently used - loadedSounds.noteSoundUsage(soundInfo); + synchronized (lock) + { + addRef(soundInfo); + loadedSounds.noteSoundUsage(soundInfo); + + boolean soundOn = isLocalSoundOn(); + final float volume = (soundOn ? soundOnVolume : soundOffVolume) * params.volume; + + checkForFinishedSounds(); + + long msNow = getMsNow(); + long msFinishNonLoopers = Long.MIN_VALUE; + for (CurrentlyPlaying currentlyPlaying : currentlyPlayingSounds) + { + if (!currentlyPlaying.isLooping()) + { + msFinishNonLoopers = Math.max(msFinishNonLoopers, currentlyPlaying.msFinish); + } + } + final long msPresentation = params.waitForNonLoopingSoundsToFinish + ? Math.max(msNow, msFinishNonLoopers) + : msNow; + long msDelay = msPresentation - msNow; + + Runnable playSound = new Runnable() + { + @Override public void run() + { + synchronized (lock) + { + long msStart = getMsNow(); + final int streamId = soundPool.play(soundInfo.sampleId, /*leftVol*/volume, /*rightVol*/volume, /*priority*/1, params.loopControl, params.rate); + boolean result = 0 != streamId; + if (result) + { + long msDuration = soundInfo.msDuration * (params.isLooping() ? 1/*don't care*/ : (params.loopControl+1)); + + CurrentlyPlaying currentlyPlaying = new CurrentlyPlaying(); + currentlyPlaying.streamId = streamId; + currentlyPlaying.loopControl = params.loopControl; + currentlyPlaying.msFinish = params.isLooping() ? Long.MAX_VALUE : msStart + msDuration; + currentlyPlaying.runWhenFinished = runWhenFinished; + currentlyPlayingSounds.add(currentlyPlaying); - // Play it for me, Sam. - playLoadedSound(soundInfo, waitForCompletion); + if (runWhenFinished != null && !params.isLooping()) + { + scheduledThreadPool.schedule(new Runnable() + { + @Override public void run() + { + checkForFinishedSounds(); + } + }, msDuration + 5*(params.loopControl+1) /*slop so it'll definitely be done by the time we check*/, TimeUnit.MILLISECONDS); + } + + tracer.trace("playing volume=%f %s", volume, soundInfo); + soundInfo.msLastPlay = msStart; + } + else + { + tracer.traceError("unable to play %s", soundInfo); + } + releaseRef(soundInfo); + + if (runWhenStarted != null) + { + threadPool.execute(new Runnable() + { + @Override public void run() + { + runWhenStarted.accept(streamId); + } + }); + } + } + if (isRobotController) + { + // Tell the driver station too! + CommandList.CmdPlaySound cmdPlaySound = new CommandList.CmdPlaySound(msPresentation, soundInfo.hashString, params); + Command command = new Command(CommandList.CmdPlaySound.Command, cmdPlaySound.serialize()); + command.setTransmissionDeadline(new Deadline(msSoundTransmissionFreshness, TimeUnit.MILLISECONDS)); + NetworkConnectionHandler.getInstance().sendCommand(command); + } + } + }; + + if (msDelay > 0) + { + // Wait for any current sound to finish playing. + scheduledThreadPool.schedule(playSound, msDelay, TimeUnit.MILLISECONDS); + } + else + playSound.run(); + + } } } @@ -266,107 +630,499 @@ protected int getMsDuration(Context context, @RawRes int resourceId) MediaPlayer mediaPlayer = MediaPlayer.create(context, resourceId); int msDuration = mediaPlayer.getDuration(); mediaPlayer.release(); - if (DEBUG) RobotLog.vv(TAG, "duration(res=0x%08x)=%d", resourceId, msDuration); + return msDuration; + } + + protected int getMsDuration(Context context, File file) + { + MediaPlayer mediaPlayer = MediaPlayer.create(context, Uri.fromFile(file)); + int msDuration = mediaPlayer.getDuration(); + mediaPlayer.release(); return msDuration; } protected void waitForLoadCompletion() { - // Wait for the load to finish - while (currentlyLoading != 0) + // Wait for the load to finish. Note that our lock is held when this is called. + try { + currentlyLoadingLatch.await(); + currentlyLoadingLatch = null; + currentlyLoadingInfo = null; + } + catch (InterruptedException e) { - Thread.yield(); + Thread.currentThread().interrupt(); } } - protected void playLoadedSound(SoundInfo soundInfo, boolean waitForCompletion) + @Override public void onLoadComplete(SoundPool soundPool, int sampleId, int status) + { + tracer.trace("onLoadComplete(%s, samp=%d)=%d", currentlyLoadingInfo, sampleId, status); + currentlyLoadingLatch.countDown(); + } + + protected long getMsNow() + { + return AppUtil.getInstance().getWallClockTime(); + } + + //---------------------------------------------------------------------------------------------- + // Remoting + //---------------------------------------------------------------------------------------------- + + protected interface SoundFromFile + { + SoundInfo apply(File file); + } + + /** returns a new ref on the returned {@link SoundInfo}; caller must releaseRef() */ + protected @CheckResult SoundInfo ensureLoaded(final String hashString, final SoundFromFile ifAbsent) { + SoundInfo soundInfo = loadedSounds.getHash(hashString); if (soundInfo != null) { - if (DEBUG) RobotLog.vv(TAG, "playLoadedSound(%d)", soundInfo.sampleId); - - boolean soundOn = sharedPreferences.getBoolean(context.getString(R.string.pref_sound_on_off), true); - float volume = soundOn ? soundOnLevel : soundOffLevel; + return soundInfo; + } + else + { + return ensureCached(hashString, ifAbsent); + } + } - if (waitForCompletion) + /** Ensures this local sound is also in the local cache. */ + protected void ensureCached(Context context, @RawRes int resId) + { + final SoundInfo soundInfo = ensureLoaded(context, resId); + if (soundInfo != null) + { + String hashString = soundInfo.hashString; + SoundInfo cachedSoundInfo = ensureCached(hashString, new SoundFromFile() + { + @Override public SoundInfo apply(File file) + { + InputStream inputStream = soundInfo.getInputStream(); + OutputStream outputStream = null; + try { + outputStream = new FileOutputStream(file); + copy(inputStream, outputStream, soundInfo.cbSize); + } + catch (IOException e) + { + tracer.traceError(e, "exception caching file: %s", file); + } + finally + { + safeClose(outputStream); + safeClose(inputStream); + } + return null; // we don't need the actual sound + } + }); + if (cachedSoundInfo != null) { - // Wait for the current sound to finish playing. - long msNow = getCurrentMilliseconds(); - long msDelay = Math.max(0, msFinishPlaying-msNow); - try { Thread.sleep(msDelay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + releaseRef(cachedSoundInfo); } + } + } + + /** returns a new ref on the returned {@link SoundInfo}; caller must releaseRef() */ + protected @CheckResult SoundInfo ensureCached(final String hashString, final SoundFromFile ifAbsent) + { + final MutableReference result = new MutableReference<>(null); + AppUtil.getInstance().ensureDirectoryExists(AppUtil.SOUNDS_CACHE, false); - // We try to successfully play for a little while before we eventually give up - long msTry = 1000; - long msDeadline = getCurrentMilliseconds() + msTry; - while (true) + // Only one of these at a time, please + FileBasedLock fileBasedLock = new FileBasedLock(AppUtil.SOUNDS_CACHE); + + try { + fileBasedLock.lockWhile(new Runnable() { - long msNow = getCurrentMilliseconds(); - if (msNow >= msDeadline) + @Override public void run() { - break; + // It's not loaded. Do we have a cache? + boolean success = false; + File file = new File(AppUtil.SOUNDS_CACHE, hashString + ".sound"); // we don't know the actual format, so can't guess an extension + if (file.exists()) + { + SoundInfo soundInfo = ensureLoaded(AppUtil.getDefContext(), file); + if (soundInfo != null) + { + result.setValue(soundInfo); + success = true; + } + } + if (!success) + { + result.setValue(ifAbsent.apply(file)); + } } + }); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + } - long msStart = msNow; - int streamId = soundPool.play(soundInfo.sampleId, /*leftVol*/volume, /*rightVol*/volume, /*priority*/1, /*loop*/0, /*rate*/1.0f); - if (streamId != 0) - { - // Started playing. Yay! - soundInfo.msLastPlay = msStart; - msFinishPlaying = Math.max(msFinishPlaying, msStart + soundInfo.msDuration); - return; - } + return result.getValue(); + } + + public CallbackResult handleCommandPlaySound(String extra) + { + CallbackResult callbackResult = CallbackResult.HANDLED; // we may not be *successful*, but not worth having others try + final CommandList.CmdPlaySound cmdPlaySound = CommandList.CmdPlaySound.deserialize(extra); + + SoundInfo soundInfo = ensureLoaded(cmdPlaySound.hashString, new SoundFromFile() + { + @Override public SoundInfo apply(File file) + { + return requestRemoteSound(file, cmdPlaySound.hashString); + } + }); + + if (soundInfo != null) + { + long msPresentation = RobotLog.getLocalTime(cmdPlaySound.msPresentationTime); + long msNow = getMsNow(); + long msDelay = msPresentation - msNow; + + // Ideally, if msDelay is positive, and if we know we're in good time synch, we'd wait until + // the presentation time to actually play the sound. But we're a little queasy about relying + // on the time synch, especially as we reduce heartbeats to save network traffic, so we omit + // that, for now at least. As a consequence, it only makes sense for the RC to sends us stuff + // it wants to play immediately, which is all that it presently ever sends, so we're OK. + // See also the command.hasExpired() check in SendOnceRunnable. + + startPlayingLoadedSound(soundInfo, cmdPlaySound.getParams(), null, null); + releaseRef(soundInfo); + } + + return callbackResult; + } + + public CallbackResult handleCommandStopPlayingSounds(Command stopPlayingSoundsCommand) + { + String extra = stopPlayingSoundsCommand.getExtra(); + CallbackResult callbackResult = CallbackResult.HANDLED; // we may not be *successful*, but not worth having others try + CommandList.CmdStopPlayingSounds cmdStopPlayingSounds = CommandList.CmdStopPlayingSounds.deserialize(extra); + tracer.trace("handleCommandStopPlayingSounds(): what=%s", cmdStopPlayingSounds.stopWhat); + internalStopPlaying(cmdStopPlayingSounds.stopWhat); + return callbackResult; + } + + SoundInfo requestRemoteSound(File file, String hashString) + { + SoundInfo result = null; + + // We need to get it from the other side + OutputStream outputStream = null; + ServerSocket serverSocket = null; + Socket clientSocket = null; + InputStream inputStream = null; + boolean deleteFile = true; + try { + // Open the file so we're ready for data + outputStream = new FileOutputStream(file); - /* if (soundInfo.msLastPlay==0) + // Start listening on an ephemeral local port + serverSocket = new ServerSocket(0); // 0 == port assigned by the OS + + // Ask the other guy to send us that sound + CommandList.CmdRequestSound cmdRequestSound = new CommandList.CmdRequestSound(hashString, serverSocket.getLocalPort()); + tracer.trace("handleCommandPlaySound(): requesting: port=%d hash=%s", cmdRequestSound.port, cmdRequestSound.hashString); + NetworkConnectionHandler.getInstance().sendCommand(new Command(CommandList.CmdRequestSound.Command, cmdRequestSound.serialize())); + + final int msTimeout = 1 * 1000; + serverSocket.setSoTimeout(msTimeout); + try { + clientSocket = serverSocket.accept(); + clientSocket.setSoTimeout(msTimeout); + + // Ok, he's sending. Suck it all in and write it to the file + inputStream = clientSocket.getInputStream(); + + // How much data is he going to send us? + byte[] buffer = new byte[4]; + if (buffer.length == inputStream.read(buffer)) { - // Give it a moment for the onLoadComplete call to return and report internally as READY - if (DEBUG) RobotLog.vv(TAG, "waiting for successful play"); - Thread.yield(); + int cbToRead = TypeConversion.byteArrayToInt(buffer); + if (cbToRead > 0) + { + copy(inputStream, outputStream, cbToRead); + safeClose(outputStream); outputStream = null; + deleteFile = false; + tracer.trace("handleCommandPlaySound(): received: hash=%s", hashString); + + result = ensureLoaded(AppUtil.getDefContext(), file); + } + else + tracer.traceError("handleCommandPlaySound(): client couldn't send sound"); } - else*/ - break; // don't repeat earlier problems + else + throw new IOException("framing error"); } + catch (SocketTimeoutException e) + { + tracer.traceError("timed out awaiting sound file"); + } + } + catch (IOException|RuntimeException e) + { + tracer.traceError(e, "handleCommandPlaySound(): exception thrown"); + } + finally + { + safeClose(inputStream); + safeClose(clientSocket); + safeClose(serverSocket); + safeClose(outputStream); + if (deleteFile) + { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + } - RobotLog.vv(TAG, "Abandoning play attempt res=0x%08x samp=%d", soundInfo.resourceId, soundInfo.sampleId); + return result; + } + + protected static void copy(InputStream inputStream, OutputStream outputStream, int cbToCopy) throws IOException + { + if (cbToCopy > 0) + { + byte[] buffer = new byte[256]; + for (;;) + { + int cbRead = inputStream.read(buffer); + if (cbRead < 0) + throw new IOException("insufficient data"); + outputStream.write(buffer, 0, cbRead); + cbToCopy -= cbRead; + if (cbToCopy <= 0) + break; + } } } - protected long getCurrentMilliseconds() + public CallbackResult handleCommandRequestSound(Command requestSoundCommand) { - return System.nanoTime() / ElapsedTime.MILLIS_IN_NANO; + String extra = requestSoundCommand.getExtra(); + CallbackResult callbackResult = CallbackResult.HANDLED; // we may not be *successful*, but not worth having others try + CommandList.CmdRequestSound cmdRequestSound = CommandList.CmdRequestSound.deserialize(extra); + tracer.trace("handleCommandRequestSound(): hash=%s", cmdRequestSound.hashString); + // + Socket socket = null; + OutputStream outputStream = null; + InputStream inputStream = null; + try { + // He told us what port to use, but the host involved is just whomever sent us the command + socket = new Socket(requestSoundCommand.getSender().getAddress(), cmdRequestSound.port); + outputStream = socket.getOutputStream(); + + SoundInfo soundInfo = loadedSounds.getHash(cmdRequestSound.hashString); + if (soundInfo != null) + { + inputStream = soundInfo.getInputStream(); + } + else + tracer.traceError("handleCommandRequestSound(): can't find hash=%s", cmdRequestSound.hashString); + + if (inputStream != null) + { + // Write framing + outputStream.write(TypeConversion.intToByteArray(soundInfo.cbSize)); + // Write data + byte[] buffer = new byte[256]; + int cbWritten = 0; + for (;;) + { + int cbRead = inputStream.read(buffer); + if (cbRead < 0) + break; + outputStream.write(buffer, 0, cbRead); + cbWritten += cbRead; + } + tracer.trace("handleCommandRequestSound(): finished: %s cbSize=%d cbWritten=%d", soundInfo, soundInfo.cbSize, cbWritten); + } + else + { + // Write error framing to unblock caller + outputStream.write(TypeConversion.intToByteArray(0)); + } + + releaseRef(soundInfo); + } + catch (IOException|RuntimeException e) + { + tracer.traceError(e, "handleCommandRequestSound(): exception thrown"); + } + finally + { + safeClose(inputStream); + safeClose(outputStream); + safeClose(socket); + } + + return callbackResult; } - /** - * Called when a sound has completed loading. - * - * @param soundPool SoundPool object from the load() method - * @param sampleId the sample ID of the sound loaded. - * @param status the status of the load operation (0 = success) - */ - @Override - public void onLoadComplete(SoundPool soundPool, int sampleId, int status) + protected void safeClose(Object closeable) { - if (DEBUG) RobotLog.vv(TAG, "onLoadComplete(res=0x%08x samp=%d)=%d", currentlyLoading, sampleId, status); - currentlyLoading = 0; + if (closeable != null) + { + try { + if (closeable instanceof Flushable) + { + try { + ((Flushable)closeable).flush(); + } + catch (IOException e) + { + tracer.traceError(e, "exception while flushing"); + } + } + + if (closeable instanceof Closeable) + { + ((Closeable) closeable).close(); + } + else + { + throw new IllegalArgumentException("Unknown object to close"); + } + } + catch (IOException e) + { + tracer.traceError(e, "exception while closing"); + } + } } //---------------------------------------------------------------------------------------------- // Types //---------------------------------------------------------------------------------------------- - protected class SoundInfo + protected class SoundInfo extends RefCounted { - public @RawRes int resourceId; - public int sampleId; - public long msDuration; - public long msLastPlay; + public final Context context; + public final @RawRes int resourceId; + public final File file; + public final long msDuration; + public int sampleId; + public String hashString; // String form of hash of the contents of the sound + public int cbSize; + public long msLastPlay = 0; + + @Override public String toString() + { + return Misc.formatInvariant("samp=%d|ms=%d", sampleId, msDuration); + } - public SoundInfo(@RawRes int resourceId, int sampleId, int msDuration) + public SoundInfo(Context context, @RawRes int resourceId, int msDuration) { + this.context = context; this.resourceId = resourceId; - this.sampleId = sampleId; + this.file = null; this.msDuration = msDuration; - this.msLastPlay = 0; + this.hashString = computeHash(); + } + + public SoundInfo(File file, int msDuration) + { + this.context = null; + this.resourceId = 0; + this.file = file; + this.msDuration = msDuration; + this.hashString = computeHash(); + } + + public void initialize(int sampleId) + { + this.sampleId = sampleId; + this.hashString = computeHash(); // also sets cbSize + } + + @Override protected void destructor() + { + tracer.trace("unloading sound %s", this); + soundPool.unload(sampleId); + super.destructor(); + } + + public @Nullable InputStream getInputStream() + { + try { + if (resourceId != 0) + { + return context.getResources().openRawResource(resourceId); + } + else + { + return new FileInputStream(file); + } + } + catch (IOException e) + { + return null; + } + } + + protected String computeHash() + { + InputStream inputStream = getInputStream(); + if (inputStream != null) + { + try { + MessageDigest digest = java.security.MessageDigest.getInstance("MD5"); + byte[] buffer = new byte[256]; + cbSize = 0; + for (;;) + { + int cbRead = inputStream.read(buffer); + if (cbRead < 0) + break; + cbSize += cbRead; + digest.update(buffer, 0, cbRead); + } + byte[] hash = digest.digest(); + StringBuilder result = new StringBuilder(); + for (int ib = 0; ib < hash.length; ib++) + { + result.append(String.format(Locale.ROOT, "%02x", hash[ib])); + } + return result.toString(); + } + catch (NoSuchAlgorithmException|IOException e) + { + tracer.traceError(e, "exception computing hash"); + } + finally + { + safeClose(inputStream); + } + } + throw Misc.illegalStateException("internal error: unable to compute hash of %s", this); // likely a bug; this will help us find + } + + public Object getKey() + { + return resourceId==0 ? file : resourceId; + } + } + + public static SoundInfo addRef(SoundInfo soundInfo) + { + if (soundInfo != null) + { + soundInfo.addRef(); + } + return soundInfo; + } + + public static void releaseRef(SoundInfo soundInfo) + { + if (soundInfo != null) + { + soundInfo.releaseRef(); } } @@ -375,56 +1131,135 @@ public SoundInfo(@RawRes int resourceId, int sampleId, int msDuration) * sound id. It keeps track of which sounds have been recently used, and unloads neglected * songs when a configured capacity of loaded sounds has been reached. */ - protected class LoadedSoundCache extends LinkedHashMap + protected class LoadedSoundCache { - static final float loadFactor = 0.75f; + //------------------------------------------------------------------------------------------ + // State + //------------------------------------------------------------------------------------------ + + private final Object lock = new Object(); + private final int capacity; // max number of cached sounds + private boolean unloadOnRemove; // whether we should unload a sound when it's removed + private final Map keyMap; + private final Map hashMap; + + class SoundInfoMap extends LinkedHashMap + { + private static final float loadFactor = 0.75f; // allow extra headroom. worth it? + + public SoundInfoMap(int capacity) + { + super((int)Math.ceil(capacity / loadFactor) + 1, loadFactor, true); + } + + @Override protected boolean removeEldestEntry(Entry eldest) + { + return size() > capacity; + } + + @Override public SoundInfo remove(Object key) + { + SoundInfo removed = super.remove(key); + if (unloadOnRemove) + { + if (removed != null) + { + releaseRef(removed); + } + } + return removed; + } + }; - final int capacity; // max number of cached sounds - boolean unloadOnRemove; // whether we should unload a sound when it's removed + //------------------------------------------------------------------------------------------ + // Construction + //------------------------------------------------------------------------------------------ LoadedSoundCache(int capacity) { - super((int)Math.ceil(capacity / loadFactor) + 1, loadFactor, true); + this.keyMap = new SoundInfoMap<>(capacity); + this.hashMap = new SoundInfoMap<>(capacity); this.capacity = capacity; this.unloadOnRemove = true; } - /** update the fact that this key has been just used, again */ - public void noteSoundUsage(SoundInfo soundInfo) + //------------------------------------------------------------------------------------------ + // Accessing + //------------------------------------------------------------------------------------------ + + public @CheckResult SoundInfo getResource(@RawRes int resourceId) { - // We're updating the MRU, we don't want to unload the sound during the remove() below. - unloadOnRemove = false; - try { - // Make this key most recently used - this.remove(soundInfo.resourceId); - put(soundInfo.resourceId, soundInfo); + synchronized (lock) + { + return addRef(keyMap.get(resourceId)); } - finally + } + + public @CheckResult SoundInfo getFile(File file) + { + synchronized (lock) { - unloadOnRemove = true; + return addRef(keyMap.get(file.getAbsoluteFile())); } } - @Override - protected boolean removeEldestEntry(Entry eldest) + public @CheckResult SoundInfo getHash(String hashString) { - return size() > capacity; + synchronized (lock) + { + return addRef(hashMap.get(hashString)); + } + } + + public void putResource(@RawRes int resourceId, SoundInfo info) + { + synchronized (lock) + { + keyMap.put(resourceId, addRef(info)); + hashMap.put(info.hashString, addRef(info)); + } } - @Override - public SoundInfo remove(Object key) + public void putFile(File file, SoundInfo info) { - SoundInfo soundInfo = super.remove(key); - if (unloadOnRemove) + synchronized (lock) { - if (soundInfo != null) + keyMap.put(file.getAbsoluteFile(), addRef(info)); + hashMap.put(info.hashString, addRef(info)); + } + } + + /** update the fact that this sound has been just used, again */ + public void noteSoundUsage(SoundInfo info) + { + synchronized (lock) + { + // We're updating the MRU, we don't want to unload the sound during the remove() below. + unloadOnRemove = false; + try { + // Make this key most recently used + Object key = info.getKey(); + keyMap.remove(key); + keyMap.put(key, info); + + hashMap.remove(info.hashString); + hashMap.put(info.hashString, info); + } + finally { - if (DEBUG) RobotLog.vv(TAG, "unloading sound 0x%08x", (Integer)key); - soundPool.unload(soundInfo.sampleId); + unloadOnRemove = true; } } - return soundInfo; } } + @Override + public void play(Context context, File file, float volume, int loop, float rate) + { + PlaySoundParams params = new PlaySoundParams(false); + params.volume = volume; + params.loopControl = loop; + params.rate = rate; + startPlaying(context, file, params, null, null); + } } diff --git a/library/src/main/java/com/qualcomm/ftccommon/USBAccessibleLynxModule.java b/library/src/main/java/com/qualcomm/ftccommon/USBAccessibleLynxModule.java index 1f0e3ec..79aed7e 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/USBAccessibleLynxModule.java +++ b/library/src/main/java/com/qualcomm/ftccommon/USBAccessibleLynxModule.java @@ -32,8 +32,13 @@ are permitted (subject to the limitations in the disclaimer below) provided that */ package com.qualcomm.ftccommon; +import android.support.annotation.NonNull; +import android.text.TextUtils; + import com.qualcomm.robotcore.util.SerialNumber; +import org.firstinspires.ftc.robotcore.internal.system.AppUtil; + /** * A simple utility class holding the serial number of a USB accessible lynx module and (optionally) its module address */ @@ -43,6 +48,7 @@ public final class USBAccessibleLynxModule protected SerialNumber serialNumber = null; protected int moduleAddress = 0; protected boolean moduleAddressChangeable = true; + protected String firmwareVersionString = ""; public USBAccessibleLynxModule(SerialNumber serialNumber) { @@ -84,4 +90,25 @@ public void setModuleAddressChangeable(boolean moduleAddressChangeable) { this.moduleAddressChangeable = moduleAddressChangeable; } + + public String getFirmwareVersionString() + { + return firmwareVersionString; + } + + public String getFinishedFirmwareVersionString() + { + String result = getFirmwareVersionString(); + if (TextUtils.isEmpty(result)) + { + result = "(" + AppUtil.getDefContext().getString(R.string.lynxUnavailableFWVersionString) + ")"; + } + return result; + } + + public void setFirmwareVersionString(@NonNull String firmwareVersionString) + { + this.firmwareVersionString = firmwareVersionString; + } + } diff --git a/library/src/main/java/com/qualcomm/ftccommon/UpdateUI.java b/library/src/main/java/com/qualcomm/ftccommon/UpdateUI.java index 4c1c90a..5d55d30 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/UpdateUI.java +++ b/library/src/main/java/com/qualcomm/ftccommon/UpdateUI.java @@ -45,11 +45,12 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.qualcomm.robotcore.util.RobotLog; import com.qualcomm.robotcore.util.ThreadPool; import com.qualcomm.robotcore.wifi.NetworkConnection; -import com.qualcomm.robotcore.wifi.WifiDirectAssistant; import org.firstinspires.ftc.ftccommon.external.RobotStateMonitor; +import org.firstinspires.ftc.robotcore.internal.network.DeviceNameListener; +import org.firstinspires.ftc.robotcore.internal.network.DeviceNameManagerFactory; import org.firstinspires.ftc.robotcore.internal.system.AppUtil; -import org.firstinspires.ftc.robotcore.internal.network.DeviceNameManager; +import org.firstinspires.ftc.robotcore.internal.network.WifiDirectDeviceNameManager; import org.firstinspires.ftc.robotcore.internal.network.NetworkStatus; import org.firstinspires.ftc.robotcore.internal.network.PeerStatus; @@ -66,11 +67,11 @@ public class Callback { DeviceNameManagerCallback deviceNameManagerCallback = new DeviceNameManagerCallback(); public Callback() { - DeviceNameManager.getInstance().registerCallback(deviceNameManagerCallback); + DeviceNameManagerFactory.getInstance().registerCallback(deviceNameManagerCallback); } public void close() { - DeviceNameManager.getInstance().unregisterCallback(deviceNameManagerCallback); + DeviceNameManagerFactory.getInstance().unregisterCallback(deviceNameManagerCallback); } public RobotStateMonitor getStateMonitor() { @@ -127,7 +128,7 @@ public void run() { }); } - public void networkConnectionUpdate(final WifiDirectAssistant.Event event) { + public void networkConnectionUpdate(final NetworkConnection.NetworkEvent event) { switch (event) { case UNKNOWN: @@ -154,7 +155,7 @@ public void networkConnectionUpdate(final WifiDirectAssistant.Event event) { } } - protected class DeviceNameManagerCallback implements DeviceNameManager.Callback { + protected class DeviceNameManagerCallback implements DeviceNameListener { @Override public void onDeviceNameChanged(String newDeviceName) { displayDeviceName(newDeviceName); } @@ -213,6 +214,7 @@ public void run() { }); } + public void updateRobotStatus(@NonNull final RobotStatus status) { robotStatus = status; if (stateMonitor != null) stateMonitor.updateRobotStatus(robotStatus); diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/ConfigurationTypeArrayAdapter.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/ConfigurationTypeArrayAdapter.java new file mode 100644 index 0000000..94297c8 --- /dev/null +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/ConfigurationTypeArrayAdapter.java @@ -0,0 +1,68 @@ +/* +Copyright (c) 2018 Noah Andrews + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of Noah Andrews nor the names of his contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package com.qualcomm.ftccommon.configuration; + +import android.content.Context; +import android.graphics.Paint; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +public class ConfigurationTypeArrayAdapter extends ArrayAdapter { + + public ConfigurationTypeArrayAdapter(@NonNull Context context, @NonNull EditActivity.ConfigurationTypeAndDisplayName[] objects) { + super(context, android.R.layout.simple_spinner_dropdown_item, objects); + } + + @NonNull @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + TextView view = (TextView) convertView; + if (view == null) { + view = (TextView) LayoutInflater.from(getContext()).inflate(android.R.layout.simple_spinner_dropdown_item, parent, false); + } + + EditActivity.ConfigurationTypeAndDisplayName item = getItem(position); + assert item != null; + + if (item.configurationType.isDeprecated()) { + view.setPaintFlags(view.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); + } + + view.setText(item.displayName); + return view; + } +} diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/ConfigureFromTemplateActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/ConfigureFromTemplateActivity.java index d990777..629db1b 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/ConfigureFromTemplateActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/ConfigureFromTemplateActivity.java @@ -42,6 +42,7 @@ are permitted (subject to the limitations in the disclaimer below) provided that import com.qualcomm.ftccommon.CommandList; import com.qualcomm.ftccommon.R; import com.qualcomm.robotcore.exception.RobotCoreException; +import com.qualcomm.robotcore.hardware.ScannedDevices; import com.qualcomm.robotcore.hardware.configuration.ControllerConfiguration; import com.qualcomm.robotcore.hardware.configuration.ReadXMLFileHandler; import com.qualcomm.robotcore.robocol.Command; diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditActivity.java index e40620e..287eda1 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditActivity.java @@ -38,7 +38,6 @@ are permitted (subject to the limitations in the disclaimer below) provided that import android.preference.PreferenceManager; import android.support.annotation.IdRes; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.text.Editable; import android.text.TextWatcher; import android.view.View; @@ -49,21 +48,22 @@ are permitted (subject to the limitations in the disclaimer below) provided that import android.widget.Spinner; import com.qualcomm.ftccommon.R; +import com.qualcomm.robotcore.hardware.ScannedDevices; import com.qualcomm.robotcore.hardware.configuration.BuiltInConfigurationType; import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; import com.qualcomm.robotcore.hardware.configuration.ConfigurationUtility; import com.qualcomm.robotcore.hardware.configuration.ControllerConfiguration; import com.qualcomm.robotcore.hardware.configuration.DeviceConfiguration; import com.qualcomm.robotcore.hardware.configuration.Utility; +import com.qualcomm.robotcore.robocol.Command; import com.qualcomm.robotcore.util.RobotLog; +import org.firstinspires.ftc.robotcore.internal.network.NetworkConnectionHandler; import org.firstinspires.ftc.robotcore.internal.system.AppUtil; import org.firstinspires.ftc.robotcore.internal.network.CallbackResult; import org.firstinspires.ftc.robotcore.internal.ui.ThemedActivity; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; import java.util.LinkedList; import java.util.List; @@ -185,7 +185,7 @@ private void handleLaunchEdit(RequestCode requestCode, Class launchClass, Bundle public static String formatSerialNumber(Context context, ControllerConfiguration controllerConfiguration) { - String result = controllerConfiguration.getSerialNumber().toString(context); + String result = controllerConfiguration.getSerialNumber().toString(); if (controllerConfiguration.getSerialNumber().isFake()) { return result; @@ -375,44 +375,27 @@ public String nameOf(String name) public String displayNameOfConfigurationType(ConfigurationType.DisplayNameFlavor flavor, ConfigurationType type) { - return type.getDisplayName(flavor, this); + return type.getDisplayName(flavor); } // Localization technique from http://www.katr.com/article_android_spinner01.php - protected class ConfigurationTypeAndDisplayName implements Comparable + protected class ConfigurationTypeAndDisplayName { public final ConfigurationType.DisplayNameFlavor flavor; public final ConfigurationType configurationType; public final String displayName; - public final Comparator comparator; - public ConfigurationTypeAndDisplayName(ConfigurationType.DisplayNameFlavor flavor, ConfigurationType configurationType, @Nullable Comparator comparator) + public ConfigurationTypeAndDisplayName(ConfigurationType.DisplayNameFlavor flavor, ConfigurationType configurationType) { this.flavor = flavor; this.configurationType = configurationType; this.displayName = displayNameOfConfigurationType(this.flavor, configurationType); - this.comparator = comparator; } @Override public String toString() { return this.displayName; } - - @Override public int compareTo(@NonNull ConfigurationTypeAndDisplayName another) - { - // Compare first by the comparator if we have one - if (comparator != null) - { - int result = comparator.compare(this.configurationType, another.configurationType); - if (result != 0) - { - return result; - } - } - // Otherwise, just compare by display names - return this.displayName.compareTo(another.displayName); - } } //---------------------------------------------------------------------------------------------- @@ -443,25 +426,16 @@ protected void localizeConfigTypeSpinnerStrings(ConfigurationType.DisplayNameFla } protected void localizeConfigTypeSpinnerTypes(ConfigurationType.DisplayNameFlavor flavor, Spinner spinner, List types) - { - localizeConfigTypeSpinnerTypes(flavor, spinner, types, null); - } - - protected void localizeConfigTypeSpinnerTypes(ConfigurationType.DisplayNameFlavor flavor, Spinner spinner, List types, @Nullable Comparator comparator) // Localize the strings in the spinner { ConfigurationTypeAndDisplayName[] pairs = new ConfigurationTypeAndDisplayName[types.size()]; for (int i = 0; i < types.size(); i++) { ConfigurationType type = types.get(i); - pairs[i] = new ConfigurationTypeAndDisplayName(flavor, type, comparator); + pairs[i] = new ConfigurationTypeAndDisplayName(flavor, type); } - // Sort the spinner alphabetically - Arrays.sort(pairs); - - ArrayAdapter newAdapter = - new ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, pairs); + ConfigurationTypeArrayAdapter newAdapter = new ConfigurationTypeArrayAdapter(this, pairs); spinner.setAdapter(newAdapter); } @@ -561,6 +535,18 @@ protected void changeDevice(View itemView, ConfigurationType type) // Networking //---------------------------------------------------------------------------------------------- + protected void sendOrInject(Command cmd) + { + if (remoteConfigure) + { + NetworkConnectionHandler.getInstance().sendCommand(cmd); + } + else + { + NetworkConnectionHandler.getInstance().injectReceivedCommand(cmd); + } + } + /** When doing remote config and we get notice that the config has changed, we need to * update our header string contents and attendant red vs grey etc coloring */ protected CallbackResult handleCommandNotifyActiveConfig(String extra) diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditAnalogInputDevicesActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditAnalogInputDevicesActivity.java index a96f6c5..b3b8c25 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditAnalogInputDevicesActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditAnalogInputDevicesActivity.java @@ -64,12 +64,19 @@ are permitted (subject to the limitations in the disclaimer below) provided that package com.qualcomm.ftccommon.configuration; import com.qualcomm.ftccommon.R; +import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; public class EditAnalogInputDevicesActivity extends EditPortListSpinnerActivity { @Override public String getTag() { return this.getClass().getSimpleName(); } public static final RequestCode requestCode = RequestCode.EDIT_ANALOG_INPUT; + @Override + protected ConfigurationType.DeviceFlavor getDeviceFlavorBeingConfigured() + { + return ConfigurationType.DeviceFlavor.ANALOG_SENSOR; + } + public EditAnalogInputDevicesActivity() { this.layoutMain = R.layout.analog_inputs; diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditAnalogOutputDevicesActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditAnalogOutputDevicesActivity.java index 13936ce..cfc79e7 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditAnalogOutputDevicesActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditAnalogOutputDevicesActivity.java @@ -64,12 +64,19 @@ are permitted (subject to the limitations in the disclaimer below) provided that package com.qualcomm.ftccommon.configuration; import com.qualcomm.ftccommon.R; +import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; public class EditAnalogOutputDevicesActivity extends EditPortListSpinnerActivity { @Override public String getTag() { return this.getClass().getSimpleName(); } public static final RequestCode requestCode = RequestCode.EDIT_ANALOG_OUTPUT; + @Override + protected ConfigurationType.DeviceFlavor getDeviceFlavorBeingConfigured() + { + return ConfigurationType.DeviceFlavor.ANALOG_OUTPUT; + } + public EditAnalogOutputDevicesActivity() { this.layoutMain = R.layout.analog_outputs; @@ -80,4 +87,5 @@ public EditAnalogOutputDevicesActivity() this.idItemEditTextResult = R.id.editTextResult; this.idItemPortNumber = R.id.port_number; } + } \ No newline at end of file diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditDeviceInterfaceModuleActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditDeviceInterfaceModuleActivity.java index efdda12..16d9a84 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditDeviceInterfaceModuleActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditDeviceInterfaceModuleActivity.java @@ -42,14 +42,10 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.qualcomm.ftccommon.R; import com.qualcomm.hardware.modernrobotics.ModernRoboticsUsbDeviceInterfaceModule; -import com.qualcomm.robotcore.hardware.configuration.BuiltInConfigurationType; -import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; +import com.qualcomm.robotcore.hardware.ControlSystem; import com.qualcomm.robotcore.hardware.configuration.DeviceConfiguration; import com.qualcomm.robotcore.hardware.configuration.DeviceInterfaceModuleConfiguration; -import com.qualcomm.robotcore.hardware.configuration.UserConfigurationType; -import com.qualcomm.robotcore.hardware.configuration.UserConfigurationTypeManager; -import java.util.LinkedList; import java.util.List; public class EditDeviceInterfaceModuleActivity extends EditUSBDeviceActivity { @@ -113,29 +109,29 @@ public void onItemClick(AdapterView parent, View view, int position, long id) DeviceConfiguration.class, deviceInterfaceModuleConfiguration.getI2cDevices(), ModernRoboticsUsbDeviceInterfaceModule.MAX_I2C_PORT_NUMBER + 1); - // - List list = new LinkedList(); - list.add(BuiltInConfigurationType.NOTHING); - list.add(BuiltInConfigurationType.IR_SEEKER_V3); - list.add(BuiltInConfigurationType.COLOR_SENSOR); - list.add(BuiltInConfigurationType.ADAFRUIT_COLOR_SENSOR); - list.add(BuiltInConfigurationType.GYRO); - list.add(BuiltInConfigurationType.I2C_DEVICE); - list.add(BuiltInConfigurationType.I2C_DEVICE_SYNCH); - list.addAll(UserConfigurationTypeManager.getInstance().allUserTypes(UserConfigurationType.Flavor.I2C)); - parameters.setConfigurationTypes(list.toArray(new ConfigurationType[list.size()])); - // + parameters.setControlSystem(ControlSystem.MODERN_ROBOTICS); handleLaunchEdit(key.requestCode, EditI2cDevicesActivity.class, parameters); } else if (key.requestCode==EditAnalogInputDevicesActivity.requestCode) { - handleLaunchEdit(key.requestCode, EditAnalogInputDevicesActivity.class, deviceInterfaceModuleConfiguration.getAnalogInputDevices()); + editSimple(key, EditAnalogInputDevicesActivity.class, deviceInterfaceModuleConfiguration.getAnalogInputDevices()); } else if (key.requestCode==EditDigitalDevicesActivity.requestCode) { - handleLaunchEdit(key.requestCode, EditDigitalDevicesActivity.class, deviceInterfaceModuleConfiguration.getDigitalDevices()); + editSimple(key, EditDigitalDevicesActivity.class, deviceInterfaceModuleConfiguration.getDigitalDevices()); } else if (key.requestCode==EditAnalogOutputDevicesActivity.requestCode) { - handleLaunchEdit(key.requestCode, EditAnalogOutputDevicesActivity.class, deviceInterfaceModuleConfiguration.getAnalogOutputDevices()); + editSimple(key, EditAnalogOutputDevicesActivity.class, deviceInterfaceModuleConfiguration.getAnalogOutputDevices()); } } }; + private EditParameters initParameters(List devices) { + EditParameters result = new EditParameters(this, DeviceConfiguration.class, devices); + result.setControlSystem(ControlSystem.MODERN_ROBOTICS); + return result; + } + + private void editSimple(DisplayNameAndRequestCode key, Class launchClass, List devices) { + EditParameters parameters = initParameters(devices); + handleLaunchEdit(key.requestCode, launchClass, parameters); + } + @Override protected void onActivityResult(int requestCodeValue, int resultCode, Intent data) { logActivityResult(requestCodeValue, resultCode, data); diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditDigitalDevicesActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditDigitalDevicesActivity.java index 112240d..43ec849 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditDigitalDevicesActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditDigitalDevicesActivity.java @@ -64,12 +64,19 @@ are permitted (subject to the limitations in the disclaimer below) provided that package com.qualcomm.ftccommon.configuration; import com.qualcomm.ftccommon.R; +import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; public class EditDigitalDevicesActivity extends EditPortListSpinnerActivity { @Override public String getTag() { return this.getClass().getSimpleName(); } public static final RequestCode requestCode = RequestCode.EDIT_DIGITAL; + @Override + protected ConfigurationType.DeviceFlavor getDeviceFlavorBeingConfigured() + { + return ConfigurationType.DeviceFlavor.DIGITAL_IO; + } + public EditDigitalDevicesActivity() { this.layoutMain = R.layout.digital_devices; diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditI2cDevicesActivityAbstract.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditI2cDevicesActivityAbstract.java index ac71def..4d4d247 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditI2cDevicesActivityAbstract.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditI2cDevicesActivityAbstract.java @@ -63,25 +63,16 @@ are permitted (subject to the limitations in the disclaimer below) provided that package com.qualcomm.ftccommon.configuration; -import android.view.View; -import android.widget.Spinner; - import com.qualcomm.ftccommon.R; -import com.qualcomm.robotcore.hardware.configuration.BuiltInConfigurationType; import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; import com.qualcomm.robotcore.hardware.configuration.DeviceConfiguration; -import java.util.Arrays; -import java.util.Comparator; - /** * {@link EditI2cDevicesActivityAbstract} manages a possibly-growable list of I2c devices. The set of * legal devices is passed in as the id of a string array in EditParameters.getResourceId(). */ public abstract class EditI2cDevicesActivityAbstract extends EditPortListSpinnerActivity { - ConfigurationType[] configurationTypes = new ConfigurationType[0]; - public EditI2cDevicesActivityAbstract() { this.layoutMain = R.layout.i2cs; @@ -93,32 +84,9 @@ public EditI2cDevicesActivityAbstract() this.idItemPortNumber = R.id.port_number; } - @Override protected void deserialize(EditParameters parameters) - { - super.deserialize(parameters); - if (parameters.getConfigurationTypes() != null) - { - this.configurationTypes = parameters.getConfigurationTypes(); - } - } - @Override - protected void localizeSpinner(View itemView) + protected ConfigurationType.DeviceFlavor getDeviceFlavorBeingConfigured() { - Spinner spinner = (Spinner) itemView.findViewById(this.idItemSpinner); - - Comparator comparator = new Comparator() - { - @Override public int compare(ConfigurationType lhs, ConfigurationType rhs) - { - // Make sure 'nothing' is first - if (lhs==rhs) return 0; - if (lhs== BuiltInConfigurationType.NOTHING) return -1; - if (rhs==BuiltInConfigurationType.NOTHING) return 1; - return 0; // they'll be distinguished using an outer level comparator - } - }; - - localizeConfigTypeSpinnerTypes(ConfigurationType.DisplayNameFlavor.Normal, spinner, Arrays.asList(this.configurationTypes), comparator); + return ConfigurationType.DeviceFlavor.I2C; } } \ No newline at end of file diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditI2cDevicesActivityLynx.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditI2cDevicesActivityLynx.java index daf497f..3cd2136 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditI2cDevicesActivityLynx.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditI2cDevicesActivityLynx.java @@ -62,12 +62,39 @@ are permitted (subject to the limitations in the disclaimer below) provided that */ package com.qualcomm.ftccommon.configuration; +import android.view.View; +import android.widget.Spinner; + +import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; +import com.qualcomm.robotcore.hardware.configuration.ConfigurationTypeManager; import com.qualcomm.robotcore.hardware.configuration.LynxI2cDeviceConfiguration; +import java.util.List; + /** * Created by bob on 2016-10-21. */ public class EditI2cDevicesActivityLynx extends EditI2cDevicesActivityAbstract { + private int i2cBus; + @Override public String getTag() { return this.getClass().getSimpleName(); } + + @Override + protected void deserialize(EditParameters parameters) + { + super.deserialize(parameters); + this.i2cBus = parameters.getI2cBus(); + } + + @Override + protected void localizeSpinner(View itemView) + { + Spinner spinner = (Spinner) itemView.findViewById(this.idItemSpinner); + + List deviceTypes = + ConfigurationTypeManager.getInstance().getApplicableConfigTypes(ConfigurationType.DeviceFlavor.I2C, controlSystem, i2cBus); + + localizeConfigTypeSpinnerTypes(ConfigurationType.DisplayNameFlavor.Normal, spinner, deviceTypes); + } } diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditLegacyModuleControllerActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditLegacyModuleControllerActivity.java index 4b5924d..f54f1a9 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditLegacyModuleControllerActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditLegacyModuleControllerActivity.java @@ -43,21 +43,23 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import android.widget.TextView; import com.qualcomm.ftccommon.R; +import com.qualcomm.robotcore.hardware.ControlSystem; import com.qualcomm.robotcore.hardware.configuration.BuiltInConfigurationType; import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; +import com.qualcomm.robotcore.hardware.configuration.ConfigurationUtility; import com.qualcomm.robotcore.hardware.configuration.ControllerConfiguration; import com.qualcomm.robotcore.hardware.configuration.DeviceConfiguration; import com.qualcomm.robotcore.hardware.configuration.MatrixConstants; import com.qualcomm.robotcore.hardware.configuration.MatrixControllerConfiguration; import com.qualcomm.robotcore.hardware.configuration.ModernRoboticsConstants; -import com.qualcomm.robotcore.hardware.configuration.MotorConfiguration; import com.qualcomm.robotcore.hardware.configuration.MotorControllerConfiguration; -import com.qualcomm.robotcore.hardware.configuration.ServoConfiguration; import com.qualcomm.robotcore.hardware.configuration.ServoControllerConfiguration; import com.qualcomm.robotcore.util.RobotLog; import com.qualcomm.robotcore.util.SerialNumber; import java.util.ArrayList; +import java.util.List; +import java.util.Locale; public class EditLegacyModuleControllerActivity extends EditUSBDeviceActivity { @@ -127,7 +129,7 @@ private View createPortView(int id, int portNumber) { LinearLayout layout = (LinearLayout) findViewById(id); View result = getLayoutInflater().inflate(R.layout.simple_device, layout, true); TextView port = (TextView) result.findViewById(R.id.portNumber); - port.setText(String.format("%d", portNumber)); + port.setText(String.format(Locale.getDefault(), "%d", portNumber)); Spinner spinner = (Spinner)result.findViewById(R.id.choiceSpinner); localizeConfigTypeSpinner(ConfigurationType.DisplayNameFlavor.Legacy, spinner); @@ -168,14 +170,14 @@ private void editController_general(DeviceConfiguration controller) { controller.setName(nameText.getText().toString()); if (controller.getConfigurationType() == BuiltInConfigurationType.MOTOR_CONTROLLER) { - EditParameters parameters = new EditParameters(this, controller, MotorConfiguration.class, ((MotorControllerConfiguration)controller).getMotors()); + EditParameters parameters = new EditParameters<>(this, controller, DeviceConfiguration.class, ((MotorControllerConfiguration)controller).getMotors()); parameters.setInitialPortNumber(ModernRoboticsConstants.INITIAL_MOTOR_PORT); - parameters.setConfigurationTypes(MotorConfiguration.getAllMotorConfigurationTypes()); handleLaunchEdit(EditLegacyMotorControllerActivity.requestCode, EditLegacyMotorControllerActivity.class, parameters); } else if (controller.getConfigurationType() == BuiltInConfigurationType.SERVO_CONTROLLER) { - EditParameters parameters = new EditParameters(this, controller, ServoConfiguration.class, ((ServoControllerConfiguration)controller).getServos()); + EditParameters parameters = new EditParameters(this, controller, DeviceConfiguration.class, ((ServoControllerConfiguration)controller).getServos()); parameters.setInitialPortNumber(ModernRoboticsConstants.INITIAL_SERVO_PORT); + parameters.setControlSystem(ControlSystem.MODERN_ROBOTICS); handleLaunchEdit(EditLegacyServoControllerActivity.requestCode, EditLegacyServoControllerActivity.class, parameters); } else if (controller.getConfigurationType() == BuiltInConfigurationType.MATRIX_CONTROLLER) { @@ -303,37 +305,25 @@ private void createController(int port, ConfigurationType newType) { String name = currentModule.getName(); - SerialNumber serialNumber = new SerialNumber(); + SerialNumber serialNumber = SerialNumber.createFake(); ConfigurationType currentType = currentModule.getConfigurationType(); if (!(currentType == newType)) { //only update the controller if it's a new choice. ControllerConfiguration newModule; if (newType == BuiltInConfigurationType.MOTOR_CONTROLLER) { - ArrayList motors = new ArrayList(); - for (int motorPortNumber = ModernRoboticsConstants.INITIAL_MOTOR_PORT; motorPortNumber <= ModernRoboticsConstants.NUMBER_OF_MOTORS; motorPortNumber++) { - motors.add(new MotorConfiguration(motorPortNumber)); - } + List motors = ConfigurationUtility.buildEmptyMotors(ModernRoboticsConstants.INITIAL_MOTOR_PORT, ModernRoboticsConstants.NUMBER_OF_MOTORS); newModule = new MotorControllerConfiguration(name, motors, serialNumber); newModule.setPort(port); } else if (newType == BuiltInConfigurationType.SERVO_CONTROLLER) { - ArrayList servos = new ArrayList(); - for (int servoPortNumber = ModernRoboticsConstants.INITIAL_SERVO_PORT; servoPortNumber <= ModernRoboticsConstants.NUMBER_OF_SERVOS; servoPortNumber++) { - servos.add(new ServoConfiguration(servoPortNumber)); - } + List servos = ConfigurationUtility.buildEmptyServos(ModernRoboticsConstants.INITIAL_SERVO_PORT, ModernRoboticsConstants.NUMBER_OF_SERVOS); newModule = new ServoControllerConfiguration(name, servos, serialNumber); newModule.setPort(port); } else if (newType == BuiltInConfigurationType.MATRIX_CONTROLLER) { - ArrayList motors = new ArrayList(); - for(int motorPortNumber = MatrixConstants.INITIAL_MOTOR_PORT; motorPortNumber <= MatrixConstants.NUMBER_OF_MOTORS; motorPortNumber++) { - motors.add(new MotorConfiguration(motorPortNumber)); - } - - ArrayList servos = new ArrayList(); - for(int servoPortNumber = MatrixConstants.INITIAL_SERVO_PORT; servoPortNumber <= MatrixConstants.NUMBER_OF_SERVOS; servoPortNumber++) { - servos.add(new ServoConfiguration(servoPortNumber)); - } + List motors = ConfigurationUtility.buildEmptyMotors(MatrixConstants.INITIAL_MOTOR_PORT, MatrixConstants.NUMBER_OF_MOTORS); + List servos = ConfigurationUtility.buildEmptyServos(MatrixConstants.INITIAL_SERVO_PORT, MatrixConstants.NUMBER_OF_SERVOS); + newModule = new MatrixControllerConfiguration(name, motors, servos, serialNumber); newModule.setPort(port); } diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditLynxModuleActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditLynxModuleActivity.java index 904b97d..d298cfb 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditLynxModuleActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditLynxModuleActivity.java @@ -40,22 +40,18 @@ are permitted (subject to the limitations in the disclaimer below) provided that import android.widget.EditText; import android.widget.ListView; +import com.qualcomm.ftccommon.CommandList; import com.qualcomm.ftccommon.R; -import com.qualcomm.robotcore.hardware.configuration.BuiltInConfigurationType; -import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; +import com.qualcomm.robotcore.hardware.ControlSystem; import com.qualcomm.robotcore.hardware.configuration.DeviceConfiguration; import com.qualcomm.robotcore.hardware.configuration.LynxConstants; import com.qualcomm.robotcore.hardware.configuration.LynxI2cDeviceConfiguration; import com.qualcomm.robotcore.hardware.configuration.LynxModuleConfiguration; -import com.qualcomm.robotcore.hardware.configuration.MotorConfiguration; -import com.qualcomm.robotcore.hardware.configuration.ServoConfiguration; -import com.qualcomm.robotcore.hardware.configuration.UserConfigurationType; -import com.qualcomm.robotcore.hardware.configuration.UserConfigurationTypeManager; -import com.qualcomm.robotcore.hardware.configuration.UserI2cSensorType; +import com.qualcomm.robotcore.robocol.Command; +import com.qualcomm.robotcore.util.RobotLog; import org.firstinspires.ftc.robotcore.internal.system.Assert; -import java.util.LinkedList; import java.util.List; /** @@ -91,12 +87,29 @@ protected void onCreate(Bundle savedInstanceState) lynxModuleConfiguration = (LynxModuleConfiguration) controllerConfiguration; lynx_module_name.addTextChangedListener(new SetNameTextWatcher(lynxModuleConfiguration)); lynx_module_name.setText(lynxModuleConfiguration.getName()); + + RobotLog.vv(TAG, "lynxModuleConfiguration.getSerialNumber()=%s", lynxModuleConfiguration.getSerialNumber()); + visuallyIdentify(); } - @Override - protected void onStart() + @Override protected void onDestroy() { - super.onStart(); + super.onDestroy(); + visuallyUnidentify(); + } + + protected void visuallyIdentify() + { + sendIdentify(true); + } + protected void visuallyUnidentify() + { + sendIdentify(false); + } + protected void sendIdentify(boolean shouldIdentify) + { + CommandList.CmdVisuallyIdentify cmdVisuallyIdentify = new CommandList.CmdVisuallyIdentify(lynxModuleConfiguration.getModuleSerialNumber(), shouldIdentify); + sendOrInject(new Command(cmdVisuallyIdentify.Command, cmdVisuallyIdentify.serialize())); } private AdapterView.OnItemClickListener editLaunchListener = new AdapterView.OnItemClickListener() @@ -113,9 +126,6 @@ public void onItemClick(AdapterView parent, View view, int position, long id) case EDIT_SERVO_LIST: editServos(key, LynxConstants.INITIAL_SERVO_PORT, EditServoListActivity.class, lynxModuleConfiguration.getServos()); break; - case EDIT_PWM_PORT: - editSimple(key, 0, EditPWMDevicesActivity.class, lynxModuleConfiguration.getPwmOutputs()); - break; case EDIT_I2C_BUS0: editI2cBus(key, 0); break; @@ -142,6 +152,7 @@ EditParameters initParameters(int initialPo { EditParameters result = new EditParameters(this, clazz, currentItems); result.setInitialPortNumber(initialPortNumber); + result.setControlSystem(ControlSystem.REV_HUB); return result; } @@ -151,9 +162,9 @@ private void editSimple(DisplayNameAndRequestCode key, int initialPort, Class la handleLaunchEdit(key.requestCode, launchClass, parameters); } - private void editServos(DisplayNameAndRequestCode key, int initialPort, Class launchClass, List devices) + private void editServos(DisplayNameAndRequestCode key, int initialPort, Class launchClass, List devices) { - EditParameters parameters = initParameters(initialPort, ServoConfiguration.class, devices); + EditParameters parameters = initParameters(initialPort, DeviceConfiguration.class, devices); handleLaunchEdit(key.requestCode, launchClass, parameters); } @@ -162,8 +173,7 @@ private void editMotors(DisplayNameAndRequestCode key) Assert.assertTrue(lynxModuleConfiguration.getMotors().size() == LynxConstants.NUMBER_OF_MOTORS); Assert.assertTrue(lynxModuleConfiguration.getMotors().get(0).getPort() == LynxConstants.INITIAL_MOTOR_PORT); // - EditParameters parameters = initParameters(LynxConstants.INITIAL_MOTOR_PORT, MotorConfiguration.class, lynxModuleConfiguration.getMotors()); - parameters.setConfigurationTypes(MotorConfiguration.getAllMotorConfigurationTypes()); + EditParameters parameters = initParameters(LynxConstants.INITIAL_MOTOR_PORT, DeviceConfiguration.class, lynxModuleConfiguration.getMotors()); handleLaunchEdit(key.requestCode, EditMotorListActivity.class, parameters); } @@ -171,32 +181,7 @@ private void editMotors(DisplayNameAndRequestCode key) private void editI2cBus(DisplayNameAndRequestCode key, int busZ) { EditParameters parameters = initParameters(0, LynxI2cDeviceConfiguration.class, lynxModuleConfiguration.getI2cDevices(busZ)); - // - List list = new LinkedList(); - list.add(BuiltInConfigurationType.I2C_DEVICE_SYNCH); - list.add(BuiltInConfigurationType.IR_SEEKER_V3); - list.add(BuiltInConfigurationType.ADAFRUIT_COLOR_SENSOR); - list.add(BuiltInConfigurationType.LYNX_COLOR_SENSOR); - list.add(BuiltInConfigurationType.COLOR_SENSOR); - list.add(BuiltInConfigurationType.GYRO); - list.add(BuiltInConfigurationType.NOTHING); - // - UserConfigurationType embeddedIMUConfigurationType = UserI2cSensorType.getLynxEmbeddedIMUType(); - for (UserConfigurationType userConfigurationType : UserConfigurationTypeManager.getInstance().allUserTypes(UserConfigurationType.Flavor.I2C)) - { - // We don't allow the embedded IMU on anything but its correct bus - if (busZ != LynxConstants.EMBEDDED_IMU_BUS) - { - if (userConfigurationType == embeddedIMUConfigurationType) - { - continue; - } - } - list.add(userConfigurationType); - } - // - parameters.setConfigurationTypes(list.toArray(new ConfigurationType[list.size()])); - // + parameters.setI2cBus(busZ); parameters.setGrowable(true); handleLaunchEdit(key.requestCode, EditI2cDevicesActivityLynx.class, parameters); } @@ -210,20 +195,15 @@ protected void onActivityResult(int requestCodeValue, int resultCode, Intent dat { if (requestCode == RequestCode.EDIT_MOTOR_LIST) { - EditParameters parameters = EditParameters.fromIntent(this, data); + EditParameters parameters = EditParameters.fromIntent(this, data); lynxModuleConfiguration.setMotors(parameters.getCurrentItems()); Assert.assertTrue(lynxModuleConfiguration.getMotors().size() == LynxConstants.NUMBER_OF_MOTORS); Assert.assertTrue(lynxModuleConfiguration.getMotors().get(0).getPort()== LynxConstants.INITIAL_MOTOR_PORT); } else if (requestCode == RequestCode.EDIT_SERVO_LIST) - { - EditParameters parameters = EditParameters.fromIntent(this, data); - lynxModuleConfiguration.setServos(parameters.getCurrentItems()); - } - else if (requestCode == RequestCode.EDIT_PWM_PORT) { EditParameters parameters = EditParameters.fromIntent(this, data); - lynxModuleConfiguration.setPwmOutputs(parameters.getCurrentItems()); + lynxModuleConfiguration.setServos(parameters.getCurrentItems()); } else if (requestCode == RequestCode.EDIT_ANALOG_INPUT) { diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditMatrixControllerActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditMatrixControllerActivity.java index 91bbc3b..b5bd6da 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditMatrixControllerActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditMatrixControllerActivity.java @@ -43,8 +43,6 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.qualcomm.ftccommon.R; import com.qualcomm.robotcore.hardware.configuration.DeviceConfiguration; import com.qualcomm.robotcore.hardware.configuration.MatrixControllerConfiguration; -import com.qualcomm.robotcore.hardware.configuration.MotorConfiguration; -import com.qualcomm.robotcore.hardware.configuration.ServoConfiguration; import java.util.List; @@ -53,8 +51,8 @@ public class EditMatrixControllerActivity extends EditActivity { @Override public String getTag() { return this.getClass().getSimpleName(); } public static final RequestCode requestCode = RequestCode.EDIT_MATRIX_CONTROLLER; private MatrixControllerConfiguration matrixControllerConfigurationConfig; - private List motors; - private List servos; + private List motors; + private List servos; private EditText controller_name; private View info_port1; diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditMotorListActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditMotorListActivity.java index e8963f8..4d958bc 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditMotorListActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditMotorListActivity.java @@ -32,24 +32,22 @@ are permitted (subject to the limitations in the disclaimer below) provided that */ package com.qualcomm.ftccommon.configuration; -import android.view.View; -import android.widget.Spinner; - import com.qualcomm.ftccommon.R; -import com.qualcomm.robotcore.hardware.configuration.BuiltInConfigurationType; import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; -import com.qualcomm.robotcore.hardware.configuration.MotorConfiguration; -import com.qualcomm.robotcore.hardware.configuration.MotorConfigurationType; - -import java.util.Arrays; -import java.util.Comparator; +import com.qualcomm.robotcore.hardware.configuration.DeviceConfiguration; +import com.qualcomm.robotcore.hardware.configuration.typecontainers.MotorConfigurationType; -public class EditMotorListActivity extends EditPortListSpinnerActivity +public class EditMotorListActivity extends EditPortListSpinnerActivity { @Override public String getTag() { return this.getClass().getSimpleName(); } - ConfigurationType[] configurationTypes = new ConfigurationType[0]; ConfigurationType unspecifiedMotorType = MotorConfigurationType.getUnspecifiedMotorType(); + @Override + protected ConfigurationType.DeviceFlavor getDeviceFlavorBeingConfigured() + { + return ConfigurationType.DeviceFlavor.MOTOR; + } + public EditMotorListActivity() { this.layoutMain = R.layout.motor_list; @@ -61,37 +59,6 @@ public EditMotorListActivity() this.idItemPortNumber = R.id.port_number; } - @Override protected void deserialize(EditParameters parameters) - { - super.deserialize(parameters); - if (parameters.getConfigurationTypes() != null) - { - this.configurationTypes = parameters.getConfigurationTypes(); - } - } - - @Override - protected void localizeSpinner(View itemView) - { - Spinner spinner = (Spinner) itemView.findViewById(this.idItemSpinner); - - Comparator comparator = new Comparator() - { - @Override public int compare(ConfigurationType lhs, ConfigurationType rhs) - { - // Make sure 'nothing' is first, and 'unspecified' is second - if (lhs==rhs) return 0; - if (lhs==BuiltInConfigurationType.NOTHING) return -1; - if (rhs==BuiltInConfigurationType.NOTHING) return 1; - if (lhs==unspecifiedMotorType) return -1; - if (rhs==unspecifiedMotorType) return 1; - return 0; // they'll be distinguished using an outer level comparator - } - }; - - localizeConfigTypeSpinnerTypes(ConfigurationType.DisplayNameFlavor.Normal, spinner, Arrays.asList(this.configurationTypes), comparator); - } - @Override protected ConfigurationType getDefaultEnabledSelection() { diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditParameters.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditParameters.java index 5112034..01ffc9f 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditParameters.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditParameters.java @@ -36,16 +36,14 @@ are permitted (subject to the limitations in the disclaimer below) provided that import android.os.Bundle; import android.support.annotation.NonNull; -import com.qualcomm.robotcore.hardware.DeviceManager; -import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; +import com.qualcomm.robotcore.hardware.ControlSystem; +import com.qualcomm.robotcore.hardware.ScannedDevices; import com.qualcomm.robotcore.hardware.configuration.DeviceConfiguration; -import com.qualcomm.robotcore.util.SerialNumber; import org.firstinspires.ftc.robotcore.internal.system.Assert; import java.io.Serializable; import java.util.ArrayList; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -90,6 +88,11 @@ public class EditParameters implements Seria */ private int maxItemCount = 0; + /** + * the I2C bus number (only applicable to REV) + */ + private int i2cBus = 0; + /** * whether the user can grow the size of the list or not */ @@ -112,9 +115,9 @@ public class EditParameters implements Seria private List extantRobotConfigurations = new ArrayList(); /** - * an array of configuration types + * the type of Control System that is being edited */ - private ConfigurationType[] configurationTypes = null; + private ControlSystem controlSystem = null; /** * optional explicit configuration @@ -236,6 +239,16 @@ public int getInitialPortNumber() return this.initialPortNumber; } + public int getI2cBus() + { + return i2cBus; + } + + public void setI2cBus(int i2cBus) + { + this.i2cBus = i2cBus; + } + public RobotConfigMap getRobotConfigMap() { return this.robotConfigMap; @@ -262,14 +275,14 @@ public void setExtantRobotConfigurations(List configurations) this.extantRobotConfigurations = configurations; } - public ConfigurationType[] getConfigurationTypes() + public ControlSystem getControlSystem() { - return this.configurationTypes; + return controlSystem; } - public void setConfigurationTypes(ConfigurationType[] configurationTypes) + public void setControlSystem(ControlSystem controlSystem) { - this.configurationTypes = configurationTypes; + this.controlSystem = controlSystem; } public RobotConfigFile getCurrentCfgFile() @@ -301,7 +314,7 @@ public Bundle toBundle() } if (this.scannedDevices != null && this.scannedDevices.size() > 0) { - result.putSerializable("scannedDevices", this.scannedDevices); + result.putString("scannedDevices", this.scannedDevices.toSerializationString()); } if (this.robotConfigMap != null && this.robotConfigMap.size() > 0) { @@ -311,9 +324,9 @@ public Bundle toBundle() { result.putString("extantRobotConfigurations", RobotConfigFileManager.serializeXMLConfigList(extantRobotConfigurations)); } - if (this.configurationTypes != null) + if (this.controlSystem != null) { - result.putSerializable("configurationTypes", this.configurationTypes); + result.putSerializable("controlSystem", this.controlSystem); } if (this.currentCfgFile != null) { @@ -322,6 +335,7 @@ public Bundle toBundle() result.putBoolean("haveRobotConfigMap", this.haveRobotConfigMapParameter); result.putInt("initialPortNumber", this.initialPortNumber); result.putInt("maxItemCount", this.maxItemCount); + result.putInt("i2cBus", this.i2cBus); result.putBoolean("growable", this.growable); result.putBoolean("isConfigDirty", this.isConfigDirty); if (this.itemClass != null) @@ -356,7 +370,7 @@ public Bundle toBundle() } else if (key.equals("scannedDevices")) { - result.scannedDevices = new ScannedDevices((HashMap) bundle.getSerializable(key)); + result.scannedDevices = ScannedDevices.fromSerializationString(bundle.getString(key)); } else if (key.equals("robotConfigMap")) { @@ -370,14 +384,9 @@ else if (key.equals("extantRobotConfigurations")) { result.extantRobotConfigurations = RobotConfigFileManager.deserializeXMLConfigList(bundle.getString(key)); } - else if (key.equals("configurationTypes")) + else if (key.equals("controlSystem")) { - Object[] objects = (Object[]) bundle.getSerializable(key); - result.configurationTypes = new ConfigurationType[objects.length]; - for (int i = 0; i < objects.length; i++) - { - result.configurationTypes[i] = (ConfigurationType)objects[i]; - } + result.controlSystem = (ControlSystem) bundle.getSerializable(key); } else if (key.equals("currentCfgFile")) { @@ -387,6 +396,10 @@ else if (key.equals("initialPortNumber")) { result.initialPortNumber = bundle.getInt(key); } + else if (key.equals("i2cBus")) + { + result.i2cBus = bundle.getInt(key); + } else if (key.equals("maxItemCount")) { result.maxItemCount = bundle.getInt(key); diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditPortListActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditPortListActivity.java index 730ab50..b356cc0 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditPortListActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditPortListActivity.java @@ -47,6 +47,7 @@ are permitted (subject to the limitations in the disclaimer below) provided that import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Locale; /** * EditPortListActivity is a helper class that assists in managing configuration of @@ -171,7 +172,7 @@ protected View createItemViewForPort(int portNumber) TextView port = (TextView) result.findViewById(idItemPortNumber); if (port != null) { - port.setText(String.format("%d", portNumber)); + port.setText(String.format(Locale.getDefault(), "%d", portNumber)); } return result; diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditPortListSpinnerActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditPortListSpinnerActivity.java index afd09e0..9953843 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditPortListSpinnerActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditPortListSpinnerActivity.java @@ -37,20 +37,26 @@ are permitted (subject to the limitations in the disclaimer below) provided that import android.widget.Spinner; import android.widget.TextView; +import com.qualcomm.robotcore.hardware.ControlSystem; import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; +import com.qualcomm.robotcore.hardware.configuration.ConfigurationTypeManager; import com.qualcomm.robotcore.hardware.configuration.DeviceConfiguration; +import java.util.List; + /** * EditPortListSpinnerActivity provides a template-driven editing of a list of spinner list items */ -public class EditPortListSpinnerActivity extends EditPortListActivity +public abstract class EditPortListSpinnerActivity extends EditPortListActivity { + protected abstract ConfigurationType.DeviceFlavor getDeviceFlavorBeingConfigured(); + //---------------------------------------------------------------------------------------------- // State //---------------------------------------------------------------------------------------------- - @Override public String getTag() { return this.getClass().getSimpleName(); } protected int idItemSpinner; + protected ControlSystem controlSystem; //---------------------------------------------------------------------------------------------- // Construction @@ -60,6 +66,13 @@ protected EditPortListSpinnerActivity() { } + @Override + protected void deserialize(EditParameters parameters) + { + super.deserialize(parameters); + this.controlSystem = parameters.getControlSystem(); + } + @Override protected View createItemViewForPort(int portNumber) { @@ -68,12 +81,16 @@ protected View createItemViewForPort(int portNumber) return itemView; } + /** + * Override if you need the 3-parameter variant of getApplicableConfigTypes() + */ protected void localizeSpinner(View itemView) { - // We assume here that the spinner already contains ConfigurationType names. If that's not - // the case, override this method and call a different form of localizeConfigTypeSpinner(). Spinner spinner = (Spinner) itemView.findViewById(idItemSpinner); - localizeConfigTypeSpinner(ConfigurationType.DisplayNameFlavor.Normal, spinner); + List deviceTypes = + ConfigurationTypeManager.getInstance().getApplicableConfigTypes(getDeviceFlavorBeingConfigured(), controlSystem); + + localizeConfigTypeSpinnerTypes(ConfigurationType.DisplayNameFlavor.Normal, spinner, deviceTypes); } @Override diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditServoListActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditServoListActivity.java index b6373ef..a628e1a 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditServoListActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditServoListActivity.java @@ -33,10 +33,18 @@ are permitted (subject to the limitations in the disclaimer below) provided that package com.qualcomm.ftccommon.configuration; import com.qualcomm.ftccommon.R; +import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; public class EditServoListActivity extends EditPortListSpinnerActivity { @Override public String getTag() { return this.getClass().getSimpleName(); } + + @Override + protected ConfigurationType.DeviceFlavor getDeviceFlavorBeingConfigured() + { + return ConfigurationType.DeviceFlavor.SERVO; + } + public EditServoListActivity() { this.layoutMain = R.layout.servo_list; diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditUSBDeviceActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditUSBDeviceActivity.java index 18b8ccd..6db36a9 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditUSBDeviceActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditUSBDeviceActivity.java @@ -39,6 +39,7 @@ are permitted (subject to the limitations in the disclaimer below) provided that import com.qualcomm.ftccommon.R; import com.qualcomm.robotcore.hardware.DeviceManager; +import com.qualcomm.robotcore.hardware.ScannedDevices; import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; import com.qualcomm.robotcore.hardware.configuration.ControllerConfiguration; import com.qualcomm.robotcore.util.SerialNumber; @@ -158,10 +159,10 @@ protected void fixConfiguration() } boolean isFixable = false; - DeviceManager.DeviceType deviceType = controllerConfiguration.toUSBDeviceType(); + DeviceManager.UsbDeviceType deviceType = controllerConfiguration.toUSBDeviceType(); // Match up extraDevices by type - for (Map.Entry pair : extraUSBDevices.entrySet()) + for (Map.Entry pair : extraUSBDevices.entrySet()) { if (pair.getValue() == deviceType) { diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/EditWebcamActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditWebcamActivity.java new file mode 100644 index 0000000..c9ab732 --- /dev/null +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/EditWebcamActivity.java @@ -0,0 +1,137 @@ +/* +Copyright (c) 2018 Robert Atkinson + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted (subject to the limitations in the disclaimer below) provided that +the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list +of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of Robert Atkinson nor the names of his contributors may be used to +endorse or promote products derived from this software without specific prior +written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS +LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESSFOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +package com.qualcomm.ftccommon.configuration; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; + +import com.qualcomm.ftccommon.R; +import com.qualcomm.robotcore.hardware.configuration.WebcamConfiguration; + +public class EditWebcamActivity extends EditUSBDeviceActivity + { + @Override public String getTag() { return this.getClass().getSimpleName(); } + public static final RequestCode requestCode = RequestCode.EDIT_USB_CAMERA; + + private WebcamConfiguration webcamConfiguration; + private EditText textCameraName; + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + setContentView(R.layout.webcam_device); + + textCameraName = (EditText) findViewById(R.id.cameraName); + + EditParameters parameters = EditParameters.fromIntent(this, getIntent()); + deserialize(parameters); + webcamConfiguration = (WebcamConfiguration) this.controllerConfiguration; + + textCameraName.addTextChangedListener(new SetNameTextWatcher(controllerConfiguration)); + textCameraName.setText(controllerConfiguration.getName()); + + showFixSwapButtons(); + } + + @Override protected void refreshSerialNumber() + { + TextView serialNumberView = (TextView) findViewById(R.id.serialNumber); + serialNumberView.setText(formatSerialNumber(this, controllerConfiguration)); + } + + @Override + protected void onStart() + { + super.onStart(); + } + + @Override + protected void onActivityResult(int requestCodeValue, int resultCode, Intent data) + { + logActivityResult(requestCodeValue, resultCode, data); + if (resultCode == RESULT_OK) + { + EditParameters parameters = EditParameters.fromIntent(this, data); + RequestCode requestCode = RequestCode.fromValue(requestCodeValue); + + if (requestCode == EditSwapUsbDevices.requestCode) + { + completeSwapConfiguration(requestCodeValue, resultCode, data); + } + else if (requestCode == EditWebcamActivity.requestCode) + { + WebcamConfiguration newModule = (WebcamConfiguration) parameters.getConfiguration(); + if (newModule != null) + { + } + } + + currentCfgFile.markDirty(); + robotConfigFileManager.setActiveConfig(currentCfgFile); + } + } + + public void onDoneButtonPressed(View v) + { + finishOk(); + } + + @Override protected void finishOk() + { + controllerConfiguration.setName(textCameraName.getText().toString()); + finishOk(new EditParameters(this, controllerConfiguration, getRobotConfigMap())); + } + + public void onCancelButtonPressed(View v) + { + finishCancel(); + } + + //---------------------------------------------------------------------------------------------- + // Fixing and swapping + //---------------------------------------------------------------------------------------------- + + public void onFixButtonPressed(View v) + { + fixConfiguration(); + } + + public void onSwapButtonPressed(View view) + { + swapConfiguration(); + } + } diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/FtcConfigurationActivity.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/FtcConfigurationActivity.java index 4f91fb0..261f3cf 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/FtcConfigurationActivity.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/FtcConfigurationActivity.java @@ -47,11 +47,11 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.qualcomm.ftccommon.CommandList; import com.qualcomm.ftccommon.R; -import com.qualcomm.hardware.HardwareDeviceManager; import com.qualcomm.robotcore.exception.DuplicateNameException; import com.qualcomm.robotcore.exception.RobotCoreException; +import com.qualcomm.robotcore.hardware.ControlSystem; import com.qualcomm.robotcore.hardware.DeviceManager; -import com.qualcomm.robotcore.hardware.LynxModuleMetaList; +import com.qualcomm.robotcore.hardware.ScannedDevices; import com.qualcomm.robotcore.hardware.configuration.BuiltInConfigurationType; import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; import com.qualcomm.robotcore.hardware.configuration.ControllerConfiguration; @@ -61,10 +61,8 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.qualcomm.robotcore.hardware.configuration.LynxModuleConfiguration; import com.qualcomm.robotcore.hardware.configuration.LynxUsbDeviceConfiguration; import com.qualcomm.robotcore.hardware.configuration.ModernRoboticsConstants; -import com.qualcomm.robotcore.hardware.configuration.MotorConfiguration; import com.qualcomm.robotcore.hardware.configuration.MotorControllerConfiguration; import com.qualcomm.robotcore.hardware.configuration.ReadXMLFileHandler; -import com.qualcomm.robotcore.hardware.configuration.ServoConfiguration; import com.qualcomm.robotcore.hardware.configuration.ServoControllerConfiguration; import com.qualcomm.robotcore.robocol.Command; import com.qualcomm.robotcore.robocol.RobocolDatagram; @@ -73,14 +71,16 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE import com.qualcomm.robotcore.util.ThreadPool; import com.qualcomm.robotcore.wifi.NetworkConnection; -import org.firstinspires.ftc.robotcore.internal.ui.UILocation; import org.firstinspires.ftc.robotcore.internal.network.CallbackResult; import org.firstinspires.ftc.robotcore.internal.network.NetworkConnectionHandler; import org.firstinspires.ftc.robotcore.internal.network.RecvLoopRunnable; +import org.firstinspires.ftc.robotcore.internal.system.Misc; +import org.firstinspires.ftc.robotcore.internal.ui.UILocation; import org.xmlpull.v1.XmlPullParser; import java.io.IOException; import java.io.StringReader; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -274,7 +274,7 @@ protected void doUSBScanAndUpdateUI() { ScannedDevices devices = future.await(); // if (devices != null) { - RobotLog.ee(TAG, "scan for devices on USB bus found %d devices", devices.size()); + RobotLog.dd(TAG, "scan for devices on USB bus found %d devices", devices.size()); // Use the results of the scan to figure out what we've got here buildRobotConfigMapFromScanned(devices); // may take awhile, maybe a second or two @@ -340,42 +340,14 @@ private RobotConfigMap buildRobotConfigMapFromScanned(RobotConfigMap existingCon RobotConfigMap newRobotConfigMap = new RobotConfigMap(); configurationUtility.resetNameUniquifiers(); - for(Map.Entry entry : scannedDevices.entrySet()) { - SerialNumber serialNumber = entry.getKey(); + for(Map.Entry entry : scannedDevices.entrySet()) { + final SerialNumber serialNumber = entry.getKey(); ControllerConfiguration controllerConfiguration = null; if (carryOver(serialNumber, existingControllers)) { RobotLog.vv(TAG, "carrying over %s", serialNumber); controllerConfiguration = existingControllers.get(serialNumber); } else { - switch (entry.getValue()) { - case MODERN_ROBOTICS_USB_DC_MOTOR_CONTROLLER: - controllerConfiguration = configurationUtility.buildNewModernMotorController(serialNumber); - break; - case MODERN_ROBOTICS_USB_SERVO_CONTROLLER: - controllerConfiguration = configurationUtility.buildNewModernServoController(serialNumber); - break; - case MODERN_ROBOTICS_USB_LEGACY_MODULE: - controllerConfiguration = configurationUtility.buildNewLegacyModule(serialNumber); - break; - case MODERN_ROBOTICS_USB_DEVICE_INTERFACE_MODULE: - controllerConfiguration = configurationUtility.buildNewDeviceInterfaceModule(serialNumber); - break; - case LYNX_USB_DEVICE: - try { - RobotLog.vv(TAG, "buildRobotConfigMapFromScanned(%s)...", serialNumber); - HardwareDeviceManager deviceManager = new HardwareDeviceManager(utility.getActivity(), null); - ThreadPool.SingletonResult discoveryFuture = this.usbScanManager.startLynxModuleEnumerationIfNecessary(serialNumber); - controllerConfiguration = configurationUtility.buildNewLynxUsbDevice(serialNumber, deviceManager, discoveryFuture); - RobotLog.vv(TAG, "...buildRobotConfigMapFromScanned(%s)", serialNumber); - } catch (InterruptedException e) { - RobotLog.ee(TAG, "interrupt in buildRobotConfigMapFromScanned(%s)", serialNumber); - Thread.currentThread().interrupt(); - } catch (RobotCoreException e) { - RobotLog.ee(TAG, e, "exception in buildRobotConfigMapFromScanned(%s)", serialNumber); - controllerConfiguration = null; - } - break; - } + controllerConfiguration = configurationUtility.buildNewControllerConfiguration(serialNumber, entry.getValue(), usbScanManager.getLynxModuleMetaListSupplier(serialNumber)); } if (controllerConfiguration != null) { controllerConfiguration.setKnownToBeAttached(true); @@ -428,15 +400,25 @@ private void populateListAndWarnDevices() { } private void warnIncompleteDevices() { - if (!getRobotConfigMap().allControllersAreBound()) { - String title = getString(R.string.notAllDevicesFoundTitle); - String message = String.format(getString(R.string.notAllDevicesFoundMessage), getString(R.string.noSerialNumber)); - utility.setFeedbackText(title, message, idFeedbackAnchor, R.layout.feedback, R.id.feedbackText0, R.id.feedbackText1, R.id.feedbackOKButton); + String title = null; + String message = null; + + if (scannedDevices.getErrorMessage() != null) { + title = getString(R.string.errorScanningDevicesTitle); + message = scannedDevices.getErrorMessage(); + } else if (!getRobotConfigMap().allControllersAreBound()) { + title = getString(R.string.notAllDevicesFoundTitle); + message = Misc.formatForUser(R.string.notAllDevicesFoundMessage, getString(R.string.noSerialNumber)); } else if (getRobotConfigMap().size() == 0){ - String title = getString(R.string.noDevicesFoundTitle); - String message = getString(R.string.noDevicesFoundMessage); - utility.setFeedbackText(title, message, idFeedbackAnchor, R.layout.feedback, R.id.feedbackText0, R.id.feedbackText1, R.id.feedbackOKButton); + title = getString(R.string.noDevicesFoundTitle); + message = getString(R.string.noDevicesFoundMessage); clearDuplicateWarning(); + } + + if (title != null || message != null) { + if (title==null) title = ""; + if (message==null) message = ""; + utility.setFeedbackText(title, message, idFeedbackAnchor, R.layout.feedback, R.id.feedbackText0, R.id.feedbackText1, R.id.feedbackOKButton); } else { utility.hideFeedbackText(idFeedbackAnchor); } @@ -501,57 +483,66 @@ private void populateList() { @Override public void onItemClick(AdapterView adapterView, View v, int pos, long arg3) { - ControllerConfiguration controllerConfiguration = (ControllerConfiguration) adapterView.getItemAtPosition(pos); - ConfigurationType itemType = controllerConfiguration.getConfigurationType(); - if (itemType == BuiltInConfigurationType.MOTOR_CONTROLLER) { - EditParameters parameters = initParameters(ModernRoboticsConstants.INITIAL_MOTOR_PORT, - MotorConfiguration.class, - controllerConfiguration, - ((MotorControllerConfiguration)controllerConfiguration).getMotors()); - parameters.setConfigurationTypes(MotorConfiguration.getAllMotorConfigurationTypes()); - handleLaunchEdit(EditMotorControllerActivity.requestCode, EditMotorControllerActivity.class, parameters); - } - else if (itemType == BuiltInConfigurationType.SERVO_CONTROLLER) { - EditParameters parameters = initParameters(ModernRoboticsConstants.INITIAL_SERVO_PORT, - ServoConfiguration.class, - controllerConfiguration, - ((ServoControllerConfiguration)controllerConfiguration).getServos()); - handleLaunchEdit(EditServoControllerActivity.requestCode, EditServoControllerActivity.class, parameters); - } - else if (itemType == BuiltInConfigurationType.LEGACY_MODULE_CONTROLLER) { - EditParameters parameters = initParameters(0, - DeviceConfiguration.class, - controllerConfiguration, - ((LegacyModuleControllerConfiguration)controllerConfiguration).getDevices()); - handleLaunchEdit(EditLegacyModuleControllerActivity.requestCode, EditLegacyModuleControllerActivity.class, parameters); - } - else if (itemType == BuiltInConfigurationType.DEVICE_INTERFACE_MODULE) { - EditParameters parameters = initParameters(0, - DeviceConfiguration.class, - controllerConfiguration, - ((DeviceInterfaceModuleConfiguration)controllerConfiguration).getDevices()); - handleLaunchEdit(EditDeviceInterfaceModuleActivity.requestCode, EditDeviceInterfaceModuleActivity.class, parameters); - } - else if (itemType == BuiltInConfigurationType.LYNX_USB_DEVICE) { - EditParameters parameters = initParameters(0, - LynxModuleConfiguration.class, - controllerConfiguration, - ((LynxUsbDeviceConfiguration)controllerConfiguration).getDevices()); - handleLaunchEdit(EditLynxUsbDeviceActivity.requestCode, EditLynxUsbDeviceActivity.class, parameters); - } + ControllerConfiguration controllerConfiguration = (ControllerConfiguration) adapterView.getItemAtPosition(pos); + ConfigurationType itemType = controllerConfiguration.getConfigurationType(); + if (itemType == BuiltInConfigurationType.MOTOR_CONTROLLER) { + EditParameters parameters = initParameters(ModernRoboticsConstants.INITIAL_MOTOR_PORT, + DeviceConfiguration.class, + controllerConfiguration, + ((MotorControllerConfiguration)controllerConfiguration).getMotors()); + handleLaunchEdit(EditMotorControllerActivity.requestCode, EditMotorControllerActivity.class, parameters); + } + else if (itemType == BuiltInConfigurationType.SERVO_CONTROLLER) { + EditParameters parameters = initParameters(ModernRoboticsConstants.INITIAL_SERVO_PORT, + DeviceConfiguration.class, + controllerConfiguration, + ((ServoControllerConfiguration)controllerConfiguration).getServos()); + parameters.setControlSystem(ControlSystem.MODERN_ROBOTICS); + handleLaunchEdit(EditServoControllerActivity.requestCode, EditServoControllerActivity.class, parameters); + } + else if (itemType == BuiltInConfigurationType.LEGACY_MODULE_CONTROLLER) { + EditParameters parameters = initParameters(0, + DeviceConfiguration.class, + controllerConfiguration, + ((LegacyModuleControllerConfiguration)controllerConfiguration).getDevices()); + handleLaunchEdit(EditLegacyModuleControllerActivity.requestCode, EditLegacyModuleControllerActivity.class, parameters); + } + else if (itemType == BuiltInConfigurationType.DEVICE_INTERFACE_MODULE) { + EditParameters parameters = initParameters(0, + DeviceConfiguration.class, + controllerConfiguration, + ((DeviceInterfaceModuleConfiguration)controllerConfiguration).getDevices()); + handleLaunchEdit(EditDeviceInterfaceModuleActivity.requestCode, EditDeviceInterfaceModuleActivity.class, parameters); + } + else if (itemType == BuiltInConfigurationType.LYNX_USB_DEVICE) { + EditParameters parameters = initParameters(0, + LynxModuleConfiguration.class, + controllerConfiguration, + ((LynxUsbDeviceConfiguration)controllerConfiguration).getDevices()); + handleLaunchEdit(EditLynxUsbDeviceActivity.requestCode, EditLynxUsbDeviceActivity.class, parameters); + } + else if (itemType == BuiltInConfigurationType.WEBCAM) { + EditParameters parameters = initParameters(controllerConfiguration); + handleLaunchEdit(EditWebcamActivity.requestCode, EditWebcamActivity.class, parameters); + } } } ); } - EditParameters initParameters(int initialPortNumber, Class clazz, ControllerConfiguration controllerConfiguration, List currentItems) { - EditParameters parameters = new EditParameters(this, controllerConfiguration, clazz, currentItems); + EditParameters initParameters(int initialPortNumber, Class itemClass, ControllerConfiguration controllerConfiguration, List currentItems) { + EditParameters parameters = new EditParameters(this, controllerConfiguration, itemClass, currentItems); parameters.setInitialPortNumber(initialPortNumber); parameters.setScannedDevices(scannedDevices); parameters.setRobotConfigMap(this.getRobotConfigMap()); return parameters; } + EditParameters initParameters(ControllerConfiguration controllerConfiguration) { + return initParameters(0, DeviceConfiguration.class, controllerConfiguration, new ArrayList()); + } + + @Override protected void onActivityResult(int requestCodeValue, int resultCode, Intent data) { try { @@ -790,7 +781,7 @@ public CallbackResult commandEvent(Command command) { } @Override - public CallbackResult onNetworkConnectionEvent(NetworkConnection.Event event) { + public CallbackResult onNetworkConnectionEvent(NetworkConnection.NetworkEvent event) { return CallbackResult.NOT_HANDLED; } diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/RequestCode.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/RequestCode.java index 0862656..1830721 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/RequestCode.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/RequestCode.java @@ -47,7 +47,8 @@ public enum RequestCode EDIT_DIGITAL(9), EDIT_ANALOG_OUTPUT(10), EDIT_LYNX_MODULE(11), EDIT_LYNX_USB_DEVICE(12), EDIT_I2C_BUS0(13), EDIT_I2C_BUS1(14), EDIT_I2C_BUS2(15), EDIT_I2C_BUS3(16), EDIT_MOTOR_LIST(17), EDIT_SERVO_LIST(18), EDIT_SWAP_USB_DEVICES(19), - EDIT_FILE(20), NEW_FILE(21), AUTO_CONFIGURE(22), CONFIG_FROM_TEMPLATE(23); + EDIT_FILE(20), NEW_FILE(21), AUTO_CONFIGURE(22), CONFIG_FROM_TEMPLATE(23), + EDIT_USB_CAMERA(24); public final int value; diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/RobotConfigFileManager.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/RobotConfigFileManager.java index d455436..8383ffa 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/RobotConfigFileManager.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/RobotConfigFileManager.java @@ -23,9 +23,6 @@ import com.qualcomm.robotcore.exception.RobotCoreException; import com.qualcomm.robotcore.hardware.configuration.ControllerConfiguration; import com.qualcomm.robotcore.hardware.configuration.ReadXMLFileHandler; -import com.qualcomm.robotcore.hardware.configuration.UserConfigurationType; -import com.qualcomm.robotcore.hardware.configuration.UserConfigurationTypeManager; -import com.qualcomm.robotcore.hardware.configuration.UserI2cSensorType; import com.qualcomm.robotcore.hardware.configuration.WriteXMLFileHandler; import com.qualcomm.robotcore.robocol.Command; import com.qualcomm.robotcore.util.RobotLog; @@ -501,8 +498,11 @@ public ArrayList getXMLTemplates() Element rootElement = builder.parseSubTree(xmlConfigTaxonomyParser); Document document = rootElement.getOwnerDocument(); - // Augment the DOM to add s corresponding to @I2cSensor elements that might be present - for (UserConfigurationType userConfigurationType : UserConfigurationTypeManager.getInstance().allUserTypes(UserConfigurationType.Flavor.I2C)) { + // TODO: Adapt this code for the new way of getting annotated types (ConfigTypeManger#getApplicableTypes). + // See issue #1249 + + /* + for (UserConfigurationType userConfigurationType : UserConfigurationTypeManager.getInstance().allUserTypes(UserConfigurationType.Flavor.I2C)) { // UserI2cSensorType userI2cSensorType = (UserI2cSensorType) userConfigurationType; Element sensor = document.createElement("Sensor"); @@ -512,7 +512,7 @@ public ArrayList getXMLTemplates() addChild(document, sensor, "BusDefault", context.getString(R.string.userSensorTypeBusDefault)); // rootElement.appendChild(sensor); - } + }*/ // Turn that augmented taxonomy into a source Source sourceConfigTaxonomy = new DOMSource(rootElement); diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/RobotConfigMap.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/RobotConfigMap.java index 5be61d2..bd73cea 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/RobotConfigMap.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/RobotConfigMap.java @@ -35,12 +35,16 @@ are permitted (subject to the limitations in the disclaimer below) provided that import android.content.Context; import com.qualcomm.robotcore.hardware.DeviceManager; +import com.qualcomm.robotcore.hardware.ScannedDevices; import com.qualcomm.robotcore.hardware.configuration.BuiltInConfigurationType; import com.qualcomm.robotcore.hardware.configuration.ConfigurationType; +import com.qualcomm.robotcore.hardware.configuration.ConfigurationUtility; import com.qualcomm.robotcore.hardware.configuration.ControllerConfiguration; import com.qualcomm.robotcore.util.RobotLog; import com.qualcomm.robotcore.util.SerialNumber; +import org.firstinspires.ftc.robotcore.internal.system.Misc; + import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; @@ -139,14 +143,14 @@ public void writeToLog(String tag, String message) RobotLog.vv(tag, "robotConfigMap: %s", message); for (ControllerConfiguration controllerConfiguration : this.controllerConfigurations()) { - RobotLog.vv(tag, " serial=%s id=0x%08x name='%s' ", controllerConfiguration.getSerialNumber().toString(), controllerConfiguration.hashCode(), controllerConfiguration.getName()); + RobotLog.vv(tag, " serial=%s id=0x%08x name='%s' ", controllerConfiguration.getSerialNumber(), controllerConfiguration.hashCode(), controllerConfiguration.getName()); } } // a debugging utility public void writeToLog(String tag, String message, ControllerConfiguration controllerConfiguration) { writeToLog(tag, message); - RobotLog.vv(tag, " :serial=%s id=0x%08x name='%s' ", controllerConfiguration.getSerialNumber().toString(), controllerConfiguration.hashCode(), controllerConfiguration.getName()); + RobotLog.vv(tag, " :serial=%s id=0x%08x name='%s' ", controllerConfiguration.getSerialNumber(), controllerConfiguration.hashCode(), controllerConfiguration.getName()); } //---------------------------------------------------------------------------------------------- @@ -184,7 +188,7 @@ public void bindUnboundControllers(ScannedDevices scannedDevices) // Invert the map, so we can easily lookup (ConfigurationType -> extra controllers) Map> extraByType = new HashMap>(); - for (Map.Entry pair : extraDevices.entrySet()) + for (Map.Entry pair : extraDevices.entrySet()) { ConfigurationType configurationType = BuiltInConfigurationType.fromUSBDeviceType(pair.getValue()); if (configurationType != BuiltInConfigurationType.UNKNOWN) @@ -294,7 +298,7 @@ public List getEligibleSwapTargets(ControllerConfigurat } // Then add others we know about from scanning but haven't added yet - for (Map.Entry entry : scannedDevices.entrySet()) + for (Map.Entry entry : scannedDevices.entrySet()) { SerialNumber serialNumber = entry.getKey(); @@ -319,9 +323,9 @@ public List getEligibleSwapTargets(ControllerConfigurat */ protected String generateName(Context context, ConfigurationType type, List resultSoFar) { - for (int i = 0; ; i++) + for (int i = ConfigurationUtility.firstNamedDeviceNumber; ; i++) { - String name = String.format("%s %d", type.getDisplayName(ConfigurationType.DisplayNameFlavor.Normal, context), i); + String name = Misc.formatForUser("%s %d", type.getDisplayName(ConfigurationType.DisplayNameFlavor.Normal), i); if (!nameExists(name, resultSoFar)) { return name; diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/ScannedDevices.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/ScannedDevices.java deleted file mode 100644 index 35e5067..0000000 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/ScannedDevices.java +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright (c) 2016 Robert Atkinson - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted (subject to the limitations in the disclaimer below) provided that -the following conditions are met: - -Redistributions of source code must retain the above copyright notice, this list -of conditions and the following disclaimer. - -Redistributions in binary form must reproduce the above copyright notice, this -list of conditions and the following disclaimer in the documentation and/or -other materials provided with the distribution. - -Neither the name of Robert Atkinson nor the names of his contributors may be used to -endorse or promote products derived from this software without specific prior -written permission. - -NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS -LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESSFOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ -package com.qualcomm.ftccommon.configuration; - -import com.qualcomm.robotcore.hardware.DeviceManager; -import com.qualcomm.robotcore.util.RobotLog; -import com.qualcomm.robotcore.util.SerialNumber; - -import java.util.HashMap; -import java.util.Map; - -/** - * {@link ScannedDevices} is a simple distinguished kind of map of serial numbers - * to device types. Simple serialization and deserialization logic is provided. - */ -public class ScannedDevices extends HashMap - { - public ScannedDevices(Map map) - { - super(map); - } - - public ScannedDevices() - { - super(); - } - - private static final String pairSeparatorWrite = "|"; - private static final String pairSeparatorSplit = "\\|"; // '|' is a a regex metachar, so we have to escape - private static final String keyValueSeparatorWrite = ","; - private static final String keyValueSeparatorSplit = ","; - - public String toSerializationString() - { - StringBuilder result = new StringBuilder(); - for (Entry entry : this.entrySet()) - { - if (result.length() > 0) result.append(pairSeparatorWrite); - result.append(entry.getKey().toString()); - result.append(keyValueSeparatorWrite); - result.append(entry.getValue().toString()); - } - return result.toString(); - } - - public static ScannedDevices fromSerializationString(String string) - { - ScannedDevices result = new ScannedDevices(); - // - string = string.trim(); // paranoia - if (string.length() > 0) - { - String[] pairs = string.split(pairSeparatorSplit); - for (String pair : pairs) - { - String[] keyValue = pair.split(keyValueSeparatorSplit); - SerialNumber serialNumber = new SerialNumber(keyValue[0]); - DeviceManager.DeviceType deviceType = DeviceManager.DeviceType.valueOf(keyValue[1]); - result.put(serialNumber, deviceType); - } - } - // - return result; - } - } diff --git a/library/src/main/java/com/qualcomm/ftccommon/configuration/USBScanManager.java b/library/src/main/java/com/qualcomm/ftccommon/configuration/USBScanManager.java index 90745e7..d2df43a 100644 --- a/library/src/main/java/com/qualcomm/ftccommon/configuration/USBScanManager.java +++ b/library/src/main/java/com/qualcomm/ftccommon/configuration/USBScanManager.java @@ -41,12 +41,14 @@ are permitted (subject to the limitations in the disclaimer below) provided that import com.qualcomm.robotcore.hardware.DeviceManager; import com.qualcomm.robotcore.hardware.LynxModuleMetaList; import com.qualcomm.robotcore.hardware.RobotCoreLynxUsbDevice; +import com.qualcomm.robotcore.hardware.ScannedDevices; import com.qualcomm.robotcore.robocol.Command; import com.qualcomm.robotcore.util.NextLock; import com.qualcomm.robotcore.util.RobotLog; import com.qualcomm.robotcore.util.SerialNumber; import com.qualcomm.robotcore.util.ThreadPool; +import org.firstinspires.ftc.robotcore.external.function.Supplier; import org.firstinspires.ftc.robotcore.internal.network.NetworkConnectionHandler; import java.util.Map; @@ -168,16 +170,36 @@ LynxModuleDiscoveryState getDiscoveryState(SerialNumber serialNumber) { synchronized (lynxModuleDiscoveryStateMap) { - LynxModuleDiscoveryState result = lynxModuleDiscoveryStateMap.get(serialNumber.toString()); + LynxModuleDiscoveryState result = lynxModuleDiscoveryStateMap.get(serialNumber.getString()); if (result == null) { result = new LynxModuleDiscoveryState(serialNumber); - lynxModuleDiscoveryStateMap.put(serialNumber.toString(), result); + lynxModuleDiscoveryStateMap.put(serialNumber.getString(), result); } return result; } } + public Supplier getLynxModuleMetaListSupplier(final SerialNumber serialNumber) + { + return new Supplier() + { + @Override public LynxModuleMetaList get() + { + LynxModuleMetaList result = null; + try + { + result = startLynxModuleEnumerationIfNecessary(serialNumber).await(); + } + catch (InterruptedException e) + { + Thread.currentThread().interrupt(); + } + return result; + } + }; + } + public ThreadPool.SingletonResult startLynxModuleEnumerationIfNecessary(final SerialNumber serialNumber) { final LynxModuleDiscoveryState discoveryState = getDiscoveryState(serialNumber); @@ -193,7 +215,7 @@ public ThreadPool.SingletonResult startLynxModuleEnumeration // Send a command to the RC to do a scan RobotLog.vv(TAG, "sending remote lynx module discovery request..."); - networkConnectionHandler.sendCommand(new Command(CommandList.CMD_DISCOVER_LYNX_MODULES, serialNumber.toString())); + networkConnectionHandler.sendCommand(new Command(CommandList.CMD_DISCOVER_LYNX_MODULES, serialNumber.getString())); // Wait for the result (forever, or until interrupted) waiter.awaitNext(); @@ -207,7 +229,7 @@ public ThreadPool.SingletonResult startLynxModuleEnumeration } else { - RobotLog.vv(TAG, "discovering lynx modules on lynx device=%s...", serialNumber.toString()); + RobotLog.vv(TAG, "discovering lynx modules on lynx device=%s...", serialNumber); LynxModuleMetaList localResult = null; try { @@ -271,11 +293,11 @@ public ThreadPool.SingletonResult startDeviceScanIfNecessary() ScannedDevices localResult = null; try { - localResult = new ScannedDevices(deviceManager.scanForUsbDevices()); + localResult = deviceManager.scanForUsbDevices(); } catch (RobotCoreException e) { - RobotLog.ee(TAG, "USB bus scan threw exception: " + e.toString()); + RobotLog.ee(TAG, e,"USB bus scan threw exception"); localResult = null; } finally diff --git a/library/src/main/java/org/firstinspires/ftc/ftccommon/external/SoundPlayingRobotMonitor.java b/library/src/main/java/org/firstinspires/ftc/ftccommon/external/SoundPlayingRobotMonitor.java index 57e63b4..f133059 100644 --- a/library/src/main/java/org/firstinspires/ftc/ftccommon/external/SoundPlayingRobotMonitor.java +++ b/library/src/main/java/org/firstinspires/ftc/ftccommon/external/SoundPlayingRobotMonitor.java @@ -43,21 +43,25 @@ are permitted (subject to the limitations in the disclaimer below) provided that import com.qualcomm.robotcore.robot.RobotStatus; import com.qualcomm.robotcore.util.RobotLog; -import org.firstinspires.ftc.robotcore.internal.system.AppUtil; +import org.firstinspires.ftc.robotcore.external.function.Consumer; import org.firstinspires.ftc.robotcore.internal.network.NetworkStatus; import org.firstinspires.ftc.robotcore.internal.network.PeerStatus; +import org.firstinspires.ftc.robotcore.internal.system.AppUtil; + +import java.util.concurrent.atomic.AtomicInteger; /** * {@link SoundPlayingRobotMonitor} is an implementation of {@link RobotStateMonitor} that * plays sounds at certain event transitions within the Robot Controller application. */ +@SuppressWarnings("WeakerAccess") public class SoundPlayingRobotMonitor implements RobotStateMonitor { //---------------------------------------------------------------------------------------------- // State //---------------------------------------------------------------------------------------------- - protected static final boolean DEBUG = false; + public static boolean DEBUG = false; protected Context context; protected RobotState robotState = RobotState.UNKNOWN; protected RobotStatus robotStatus = RobotStatus.UNKNOWN; @@ -65,6 +69,8 @@ public class SoundPlayingRobotMonitor implements RobotStateMonitor protected PeerStatus peerStatus = PeerStatus.UNKNOWN; protected String errorMessage = null; protected String warningMessage = null; + protected Sound lastSoundPlayed = Sound.None; + protected AtomicInteger runningsInFlight = new AtomicInteger(0); // Identity of the sounds played by this monitor. Users can change these // instance variables in order to cause different sounds to be played. @@ -74,6 +80,8 @@ public class SoundPlayingRobotMonitor implements RobotStateMonitor public @RawRes int soundWarning = R.raw.warningmessage; public @RawRes int soundError = R.raw.errormessage; + protected enum Sound { None, Connect, Disconnect, Running, Warning, Error } + //---------------------------------------------------------------------------------------------- // Construction //---------------------------------------------------------------------------------------------- @@ -87,10 +95,66 @@ public SoundPlayingRobotMonitor(Context context) this.context = context; } + public static void prefillSoundCache() + { + SoundPlayer.getInstance().prefillSoundCache(R.raw.chimeconnect, R.raw.chimedisconnect, R.raw.nxtstartupsound, R.raw.warningmessage, R.raw.errormessage); + } + //---------------------------------------------------------------------------------------------- // Notifications //---------------------------------------------------------------------------------------------- + protected void playConnect() + { + if (!SoundPlayer.getInstance().isLocalSoundOn()) + { + // If the last sound played is 'running', but that sound was in fact transmitted + // to the remote before this 'connect' happened, then (probably) the remote didn't + // hear the 'running', so send it out again. This is a pretty reliable but not + // perfect heuristic. Fortunately, the failure mode is only that a sound is repeated, + // and we can live with that. + if (lastSoundPlayed==Sound.Running) + { + if (runningsInFlight.get() == 0) + { + RobotLog.vv(SoundPlayer.TAG, "playing running again"); + playRunning(); + } + } + } + + playSound(Sound.Connect, soundConnect); + } + + protected void playDisconnect() + { + playSound(Sound.Disconnect,soundDisconnect); + } + + protected void playRunning() + { + runningsInFlight.getAndIncrement(); + // This might be better decrementing on 'finish' instead of 'start', but all the testing has + // been done on 'start' so we'll leave it that way for now. + playSound(Sound.Running, soundRunning, new Consumer() + { + @Override public void accept(Integer nonZeroOnSuccess) + { + runningsInFlight.decrementAndGet(); + } + }, null); + } + + protected void playWarning() + { + playSound(Sound.Warning, soundWarning); + } + + protected void playError() + { + playSound(Sound.Error, soundError); + } + @Override public synchronized void updateRobotState(@NonNull RobotState robotState) { if (robotState != this.robotState) @@ -104,10 +168,7 @@ public SoundPlayingRobotMonitor(Context context) case EMERGENCY_STOP: break; default: break; case RUNNING: - // Make messages always sound after we transition to running - errorMessage = null; - warningMessage = null; - playSound(soundRunning); + playRunning(); break; } } @@ -136,8 +197,8 @@ public SoundPlayingRobotMonitor(Context context) switch (peerStatus) { case UNKNOWN: break; - case CONNECTED: if (this.peerStatus != PeerStatus.CONNECTED) playSound(soundConnect); break; - case DISCONNECTED: if (this.peerStatus != PeerStatus.DISCONNECTED) playSound(soundDisconnect); break; + case CONNECTED: if (this.peerStatus != PeerStatus.CONNECTED) playConnect(); break; + case DISCONNECTED: if (this.peerStatus != PeerStatus.DISCONNECTED) playDisconnect(); break; default: break; } } @@ -168,7 +229,7 @@ public SoundPlayingRobotMonitor(Context context) if (errorMessage != null && !errorMessage.equals(this.errorMessage)) { if (DEBUG) RobotLog.vv(SoundPlayer.TAG, "updateErrorMessage()"); - playSound(soundError); + playError(); } this.errorMessage = errorMessage; } @@ -178,13 +239,19 @@ public SoundPlayingRobotMonitor(Context context) if (warningMessage != null && !warningMessage.equals(this.warningMessage)) { if (DEBUG) RobotLog.vv(SoundPlayer.TAG, "updateWarningMessage()"); - playSound(soundWarning); + playWarning(); } this.warningMessage = warningMessage; } - protected void playSound(@RawRes final int resourceId) + protected void playSound(Sound sound,@RawRes final int resourceId) + { + playSound(sound, resourceId, null, null); + } + + protected void playSound(Sound sound, @RawRes final int resourceId, @Nullable Consumer runWhenStarted, @Nullable Runnable runWhenFinished) { - SoundPlayer.getInstance().play(context, resourceId); + lastSoundPlayed = sound; + SoundPlayer.getInstance().startPlaying(context, resourceId, new SoundPlayer.PlaySoundParams(true), runWhenStarted, runWhenFinished); } } diff --git a/library/src/main/java/org/firstinspires/ftc/ftccommon/internal/ProgramAndManageActivity.java b/library/src/main/java/org/firstinspires/ftc/ftccommon/internal/ProgramAndManageActivity.java index 1a0e50e..8b419f4 100644 --- a/library/src/main/java/org/firstinspires/ftc/ftccommon/internal/ProgramAndManageActivity.java +++ b/library/src/main/java/org/firstinspires/ftc/ftccommon/internal/ProgramAndManageActivity.java @@ -63,7 +63,7 @@ are permitted (subject to the limitations in the disclaimer below) provided that import com.qualcomm.ftccommon.R; import com.qualcomm.robotcore.util.RobotLog; -import org.firstinspires.ftc.robotcore.external.Consumer; +import org.firstinspires.ftc.robotcore.external.function.Consumer; import org.firstinspires.ftc.robotcore.internal.system.AppUtil; import org.firstinspires.ftc.robotcore.internal.system.AppUtil.DialogContext; import org.firstinspires.ftc.robotcore.internal.system.Assert; @@ -80,6 +80,7 @@ are permitted (subject to the limitations in the disclaimer below) provided that import java.net.URI; import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -314,7 +315,7 @@ protected void showFileChooser(ValueCallback filePathCallback, @Nullable { URI uri = URI.create(consoleMessage.sourceId()); RobotLog.dd(TAG, "%s(%s,%d): %s", - consoleMessage.messageLevel().toString().toLowerCase(), + consoleMessage.messageLevel().toString().toLowerCase(Locale.getDefault()), uri.getPath(), consoleMessage.lineNumber(), consoleMessage.message()); @@ -399,15 +400,25 @@ else if (RobotControllerWebHandlers.URI_TOAST.equals(url.getPath())) protected DialogContext showAlert(String message, @Nullable Consumer runOnDismiss) { - return AppUtil.getInstance().showDialog(UILocation.ONLY_LOCAL, AppUtil.DialogFlavor.ALERT, this, getString(R.string.alertTitleRobotControllerConsole), message, null, runOnDismiss); + AppUtil.DialogParams params = new AppUtil.DialogParams(UILocation.ONLY_LOCAL, getString(R.string.alertTitleRobotControllerConsole), message); + params.activity = this; + params.flavor = AppUtil.DialogFlavor.ALERT; + return AppUtil.getInstance().showDialog(params, runOnDismiss); } protected DialogContext showConfirm(String message, @Nullable Consumer runOnDismiss) { - return AppUtil.getInstance().showDialog(UILocation.ONLY_LOCAL, AppUtil.DialogFlavor.CONFIRM, this, getString(R.string.alertTitleRobotControllerConsole), message, null, runOnDismiss); + AppUtil.DialogParams params = new AppUtil.DialogParams(UILocation.ONLY_LOCAL, getString(R.string.alertTitleRobotControllerConsole), message); + params.activity = this; + params.flavor = AppUtil.DialogFlavor.CONFIRM; + return AppUtil.getInstance().showDialog(params, runOnDismiss); } protected DialogContext showPrompt(String message, @Nullable String defaultValue, @Nullable Consumer runOnDismiss) { - return AppUtil.getInstance().showDialog(UILocation.ONLY_LOCAL, AppUtil.DialogFlavor.PROMPT, this, getString(R.string.alertTitleRobotControllerConsole), message, defaultValue, runOnDismiss); + AppUtil.DialogParams params = new AppUtil.DialogParams(UILocation.ONLY_LOCAL, getString(R.string.alertTitleRobotControllerConsole), message); + params.activity = this; + params.flavor = AppUtil.DialogFlavor.PROMPT; + params.defaultValue = defaultValue; + return AppUtil.getInstance().showDialog(params, runOnDismiss); } // https://stackoverflow.com/questions/33434532/android-webview-download-files-like-browsers-do diff --git a/library/src/main/res/layout/activity_about.xml b/library/src/main/res/layout/activity_about.xml deleted file mode 100644 index d5497bf..0000000 --- a/library/src/main/res/layout/activity_about.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - diff --git a/library/src/main/res/layout/activity_ftc_lynx_fw_update.xml b/library/src/main/res/layout/activity_ftc_lynx_fw_update.xml index 568d6eb..d077e34 100644 --- a/library/src/main/res/layout/activity_ftc_lynx_fw_update.xml +++ b/library/src/main/res/layout/activity_ftc_lynx_fw_update.xml @@ -10,13 +10,29 @@ tools:context=".FtcLynxFirmwareUpdateActivity" > + + + + + diff --git a/library/src/main/res/layout/analog_input_device.xml b/library/src/main/res/layout/analog_input_device.xml index a7aa58f..8b2cf28 100644 --- a/library/src/main/res/layout/analog_input_device.xml +++ b/library/src/main/res/layout/analog_input_device.xml @@ -21,7 +21,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:drawable/btn_dropdown" - android:entries="@array/choice_array_analogInput" android:prompt="@string/choice_prompt_analogInput" android:spinnerMode="dropdown" /> diff --git a/library/src/main/res/layout/analog_output_device.xml b/library/src/main/res/layout/analog_output_device.xml index 232d0b7..f8dda75 100644 --- a/library/src/main/res/layout/analog_output_device.xml +++ b/library/src/main/res/layout/analog_output_device.xml @@ -22,7 +22,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:drawable/btn_dropdown" - android:entries="@array/choice_array_analogOutput" android:prompt="@string/choice_prompt_analogOutput" android:spinnerMode="dropdown" /> diff --git a/library/src/main/res/layout/device_interface_module.xml b/library/src/main/res/layout/device_interface_module.xml index 0915cf0..fd774e3 100644 --- a/library/src/main/res/layout/device_interface_module.xml +++ b/library/src/main/res/layout/device_interface_module.xml @@ -33,7 +33,7 @@ android:hint="@string/name_prompt_text" android:inputType="text" android:maxLength="200" - android:text="@string/deviceInterfaceModuleName" + android:text="@string/filler_text" android:textSize="18sp" android:textStyle="bold"> diff --git a/library/src/main/res/layout/digital_device.xml b/library/src/main/res/layout/digital_device.xml index 7380838..7b00c74 100644 --- a/library/src/main/res/layout/digital_device.xml +++ b/library/src/main/res/layout/digital_device.xml @@ -21,7 +21,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:drawable/btn_dropdown" - android:entries="@array/choice_array_digital_device" android:prompt="@string/choice_prompt_digital_device" android:spinnerMode="dropdown" /> diff --git a/library/src/main/res/layout/digital_device_lynx.xml b/library/src/main/res/layout/digital_device_lynx.xml index 0bf1637..7b00c74 100644 --- a/library/src/main/res/layout/digital_device_lynx.xml +++ b/library/src/main/res/layout/digital_device_lynx.xml @@ -21,7 +21,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:drawable/btn_dropdown" - android:entries="@array/choice_array_digital_device_lynx" android:prompt="@string/choice_prompt_digital_device" android:spinnerMode="dropdown" /> diff --git a/library/src/main/res/layout/i2cs.xml b/library/src/main/res/layout/i2cs.xml index 7ef7d83..46335e6 100644 --- a/library/src/main/res/layout/i2cs.xml +++ b/library/src/main/res/layout/i2cs.xml @@ -3,7 +3,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" - tools:context=".EditDigitalDevicesActivity"> + tools:context=".configuration.EditI2cDevicesActivityAbstract"> @@ -52,6 +52,7 @@ android:id="@+id/controller_name_prompt_text" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginStart="8dp" android:layout_marginBottom="30dp" android:text="@string/legacy_controller_name"> diff --git a/library/src/main/res/layout/lynx_module.xml b/library/src/main/res/layout/lynx_module.xml index 3b9e40b..1300885 100644 --- a/library/src/main/res/layout/lynx_module.xml +++ b/library/src/main/res/layout/lynx_module.xml @@ -31,7 +31,7 @@ android:hint="@string/name_prompt_text" android:inputType="text" android:maxLength="200" - android:text="@string/configTypeLynxModule" + android:text="@string/filler_text" android:textSize="18sp" android:textStyle="bold"> diff --git a/library/src/main/res/layout/lynx_usb_device.xml b/library/src/main/res/layout/lynx_usb_device.xml index 05e3548..1c117b4 100644 --- a/library/src/main/res/layout/lynx_usb_device.xml +++ b/library/src/main/res/layout/lynx_usb_device.xml @@ -31,7 +31,7 @@ android:hint="@string/name_prompt_text" android:inputType="text" android:maxLength="200" - android:text="@string/servo_controller_name" + android:text="@string/filler_text" android:textSize="18sp" android:textStyle="bold"> diff --git a/library/src/main/res/layout/matrices.xml b/library/src/main/res/layout/matrices.xml index 50f1de3..71bf46b 100644 --- a/library/src/main/res/layout/matrices.xml +++ b/library/src/main/res/layout/matrices.xml @@ -36,7 +36,7 @@ android:hint="@string/name_prompt_text" android:inputType="text" android:maxLength="200" - android:text="@string/matrix_controller_name" + android:text="@string/filler_text" android:textSize="18sp" android:textStyle="bold"> @@ -45,6 +45,7 @@ android:id="@+id/controller_name_prompt_text" android:layout_width="match_parent" android:layout_height="wrap_content" + android:layout_marginStart="8dp" android:layout_marginBottom="30dp" android:text="@string/matrix_controller_name_prompt"> diff --git a/library/src/main/res/layout/motor_controller_banner.xml b/library/src/main/res/layout/motor_controller_banner.xml index c9e0528..150b032 100644 --- a/library/src/main/res/layout/motor_controller_banner.xml +++ b/library/src/main/res/layout/motor_controller_banner.xml @@ -12,7 +12,7 @@ android:hint="@string/name_prompt_text" android:inputType="text" android:maxLength="200" - android:text="@string/motor_controller_name" + android:text="@string/filler_text" android:textSize="18sp" android:textStyle="bold"> @@ -28,6 +28,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="8dp" + android:layout_marginStart="8dp" android:text="@string/motor_controller_name_prompt"> diff --git a/library/src/main/res/layout/servo.xml b/library/src/main/res/layout/servo.xml index 939dd20..56028c4 100644 --- a/library/src/main/res/layout/servo.xml +++ b/library/src/main/res/layout/servo.xml @@ -23,7 +23,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@android:drawable/btn_dropdown" - android:entries="@array/choice_array_servo" android:prompt="@string/choice_prompt_servo" android:spinnerMode="dropdown"/> diff --git a/library/src/main/res/layout/servo_controller_banner.xml b/library/src/main/res/layout/servo_controller_banner.xml index b80a7c8..01c5620 100644 --- a/library/src/main/res/layout/servo_controller_banner.xml +++ b/library/src/main/res/layout/servo_controller_banner.xml @@ -12,7 +12,7 @@ android:hint="@string/name_prompt_text" android:inputType="text" android:maxLength="200" - android:text="@string/servo_controller_name" + android:text="@string/filler_text" android:textSize="18sp" android:textStyle="bold"> @@ -28,6 +28,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="8dp" + android:layout_marginStart="8dp" android:text="@string/servo_controller_name_prompt"> diff --git a/library/src/main/res/layout/webcam_device.xml b/library/src/main/res/layout/webcam_device.xml new file mode 100644 index 0000000..9c5009e --- /dev/null +++ b/library/src/main/res/layout/webcam_device.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library/src/main/res/values/values.xml b/library/src/main/res/values/values.xml index d740d20..dd69ed2 100644 --- a/library/src/main/res/values/values.xml +++ b/library/src/main/res/values/values.xml @@ -1,27 +1,7 @@ - - NOTHING - OPTICAL_DISTANCE_SENSOR - ANALOG_INPUT - MR_ANALOG_TOUCH_SENSOR - - - NOTHING - ANALOG_OUTPUT - - - NOTHING - TOUCH_SENSOR - LED - DIGITAL_DEVICE - - - NOTHING - LED - DIGITAL_DEVICE - + NOTHING GYRO TOUCH_SENSOR COMPASS @@ -34,12 +14,6 @@ MATRIX_CONTROLLER TOUCH_SENSOR_MULTIPLEXER COLOR_SENSOR - NOTHING - - - NOTHING - SERVO - CONTINUOUS_ROTATION_SERVO PWM Devices|EDIT_PWM_PORT @@ -52,7 +26,6 @@ Motors|EDIT_MOTOR_LIST Servos|EDIT_SERVO_LIST Digital Devices|EDIT_DIGITAL - PWM Devices|EDIT_PWM_PORT Analog Input Devices|EDIT_ANALOG_INPUT I2C Bus 0|EDIT_I2C_BUS0 I2C Bus 1|EDIT_I2C_BUS1 @@ -65,7 +38,7 @@ App Version App Build Time Library Version - Network Connection Config Information + Network Connection Info Robot Wifi Protocol Version Connected Wifi Direct Config Information @@ -83,6 +56,7 @@ Add Servo Controller Firmware update of Expansion Hub %s failed. Please try again. Firmware Update Failed + Firmware update of Expansion Hub %s timed out. Please try again. Your device lacks system support for selecting files. Robot Controller Console FtcConfiguration @@ -148,16 +122,18 @@ Edit I2c Devices Edit Legacy Module Controller Edit Expansion Hub - Edit Master Expansion Hub + Edit Expansion Hub Portal Edit Matrix Controller Edit Motor Controller Edit Motor Controller Edit PWM Devices Edit Servo Controller Swap USB Devices + Edit Camera "Configuration not found: %s" Failed to open the Device Manager "Error parsing configuration: %s" + Error Scanning for Devices Error: %s Warning: %s Updating Expansion Hub %1$s firmware with \"%2$s\". Please do not unplug. @@ -166,10 +142,12 @@ Edit Choose Configuration File Filename + lorem ipsum Unable to fix \"%1$s\": no unique replacement %2$s attached Unable to fix \"%1$s\": no replacement %2$s attached Scanning… The scan for devices on the USB bus failed. + About unable to create robot failed to start robot Enter device name here @@ -185,10 +163,15 @@ No Expansion Hubs are connected. Please connect one or more Expansion Hubs, and try again. Each Expansion Hub connected to a robot controller over the same USB connection (wired or embedded) must have a hub address which is unique within that connection. This screen allows these persistent hub addresses to be changed. Expansion Hubs with the following serial numbers and addresses are currently connected. To change the address of a hub, select a new address from the dropdown. + Current Firmware: %s + Expansion Hub + Module address: %d + Module address: (unavailable) No Expansion Hub firmware update files were found on the robot controller. Please download firmware updates to \"%s\", and try again. - Expansion Hub firmware update file \"%s\" was found, but no Expansion Hubs are connected. Please connect one or more Expansion Hubs, and try again. - Expansion Hub firmware update file \"%1$s\" was found, as were Expansion Hubs with the following serial numbers:\n\n%2$s\nClick on the button below to update the firmware of these Expansion Hubs. - Matrix Controller + Expansion Hub firmware update file \"%s\" was found, but no Expansion Hubs are connected. Please connect one or more Expansion Hubs via USB, and try again. + Serial number: %s + Expansion Hub firmware update file \"%1$s\" was found, as were the following Expansion Hubs: + Click on the button below to update the firmware on each of these Expansion Hubs.\n\nWARNING: While the firmware update is in progress, do not unplug hubs or restart the robot, or a hub may become permanently unusable. Enter the name for this matrix controller here Motors Matrix name @@ -202,7 +185,6 @@ 4 Servos Motor - Motor Controller Enter the name for this motor controller here Enter motor name here Motor name @@ -229,6 +211,9 @@ Some Devices Not Found Port pref_hardware_config_filename + Wifi Auto Mute + Wifi Auto Mute + Turn wifi off automatically after a period of inactivity. read-only Read-only configurations can be edited, but must be saved under a new name. Select an XML file to instantiate a Robot from that file @@ -249,7 +234,6 @@ Save Changes? Save Configuration " (not attached)" - Servo Controller Enter the name for this servo controller here Enter servo name here Servo name @@ -293,6 +277,7 @@ Firmware update of Expansion Hub %s succeeded. Restart Robot Complete Restarting Robot + Robot Setup Complete Saved Unable to launch ZTE WifiChannelEditor Downloading %1$s @@ -303,6 +288,7 @@ port View Logs Download file... + Enter the name for this webcam here The Wifi-Direct device name contains non-printable characters. Please go into the Wifi-Direct settings menu and rename this device. Please wait while we update the Wifi Direct settings on this device. Select an XML file to instantiate a Robot from that file diff --git a/library/src/main/res/xml/ftc_about_activity.xml b/library/src/main/res/xml/ftc_about_activity.xml new file mode 100644 index 0000000..77f5f37 --- /dev/null +++ b/library/src/main/res/xml/ftc_about_activity.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/library/src/main/res/xml/ftc_about_activity_rc.xml b/library/src/main/res/xml/ftc_about_activity_rc.xml new file mode 100644 index 0000000..b85d429 --- /dev/null +++ b/library/src/main/res/xml/ftc_about_activity_rc.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From b110b039ffc1001b17f314e4689a612cbb3878d8 Mon Sep 17 00:00:00 2001 From: Mitchell Skaggs Date: Sun, 7 Oct 2018 13:17:28 -0500 Subject: [PATCH 2/2] Fix Gradle project sync --- build.gradle | 2 +- library/src/main/AndroidManifest.xml | 15 +-------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/build.gradle b/build.gradle index d04a532..d5ca043 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:3.2.0' - classpath 'com.github.dcendents:android-maven-gradle-plugin:2.0' + classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index c27c068..e190949 100644 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -1,13 +1,7 @@ - - + package="com.qualcomm.ftccommon"> - - -