diff --git a/android-wsbridge/app/src/main/java/com/dji/wsbridge/BridgeActivity.java b/android-wsbridge/app/src/main/java/com/dji/wsbridge/BridgeActivity.java index f55fa96..2bb173a 100644 --- a/android-wsbridge/app/src/main/java/com/dji/wsbridge/BridgeActivity.java +++ b/android-wsbridge/app/src/main/java/com/dji/wsbridge/BridgeActivity.java @@ -30,6 +30,7 @@ import android.widget.Toast; import com.dji.wsbridge.lib.BridgeApplication; +import com.dji.wsbridge.lib.BridgeToUSBRunner; import com.dji.wsbridge.lib.BridgeUpdateService; import com.dji.wsbridge.lib.DJILogger; import com.dji.wsbridge.lib.StreamRunner; @@ -77,7 +78,7 @@ public class BridgeActivity extends Activity { private OutputStream usbOutputStream; private InputStream wsInputStream; private OutputStream wsOutputStream; - private StreamRunner deviceToWSRunner; + private BridgeToUSBRunner deviceToWSRunner; private StreamRunner wsToDeviceRunner; private ImageButton btnSettings, btnInstallUpdate; private BridgeUpdateService bridgeUpdateService; @@ -317,7 +318,7 @@ private void refreshRunners() { if (setupStreams()) { Log.d(TAG, "Starting Runners"); isStreamRunnerActive.set(true); - deviceToWSRunner = new StreamRunner(wsInputStream, usbOutputStream, "Bridge to USB"); + deviceToWSRunner = new BridgeToUSBRunner(wsInputStream, usbOutputStream, "Bridge to USB"); wsToDeviceRunner = new StreamRunner(usbInputStream, wsOutputStream, "USB to Bridge"); try { //import static com.dji.wsbridge.lib.Utils.recordExceptionToFirebase;Firebase("Device to WS Runner alive " + deviceToWSRunner.isAlive()); diff --git a/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/BridgeToUSBRunner.java b/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/BridgeToUSBRunner.java new file mode 100644 index 0000000..7de94fb --- /dev/null +++ b/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/BridgeToUSBRunner.java @@ -0,0 +1,102 @@ +package com.dji.wsbridge.lib; + +import android.util.Log; +import com.dji.wsbridge.lib.connection.USBConnectionManager; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.atomic.AtomicBoolean; + +public class BridgeToUSBRunner extends Thread { + + private static final String TAG = StreamRunner.class.getSimpleName(); + public long byteCount = 0; + // 从web-socket 读 + private InputStream mInputStream; + // 往usb中写 + private OutputStream mOutputStream; + private AtomicBoolean mStop = new AtomicBoolean(false); + private DJIPluginRingBufferParser parser; + + public BridgeToUSBRunner(InputStream in, OutputStream out, String name) { + super(name); + mInputStream = in; + mOutputStream = out; + parser = new DJIPluginRingBufferParser(100 * 1024, mOutputStream); + } + + @Override + public void run() { + int ret; + USBConnectionManager.UsbModel usbModel = USBConnectionManager.getInstance().getUSBModel(); + // As explained in: + // https://developer.android.com/guide/topics/connectivity/usb/accessory.html + // "The Android accessory protocol supports packet buffers up to 16384 bytes, + // so you can choose to always declare your buffer to be of this size for simplicity." + byte[] buffer = new byte[16384]; + + while (!mStop.get()) { + try { + if (mOutputStream != null && mInputStream != null) { + ret = mInputStream.read(buffer); + if (ret < 0) { + // Do nothing since it is empty + Log.d(TAG, getName() + ": ret is less than 0"); + break; + } else { + Log.d(TAG, getName() + ": Runner is running"); + if (usbModel != USBConnectionManager.UsbModel.LOGIC_LINK) { + byteCount += ret; + mOutputStream.write(buffer, 0, ret); + } else { +// parser.parse(buffer, 0, ret); + + + int v1RealDataLength = ret + 8; + byte[] box_head = new byte[v1RealDataLength]; + int it = 0; + + box_head[it] = LOGIC_LINK_HEADER_0X55;it++; + box_head[it] = LOGIC_LINK_HEADER_0XCC;it++; + + box_head[it] = (byte) (CHANNEL_CMD & 0xff);it++; + box_head[it] = (byte) ((CHANNEL_CMD & 0xff00) >> 8);it++; + + box_head[it] = (byte) (ret & 0xff);it++; + box_head[it] = (byte) ((ret & 0xff00) >> 8);it++; + box_head[it] = (byte) ((ret & 0xff0000) >> 16);it++; + box_head[it] = (byte) ((ret & 0xff000000) >> 24);it++; + + System.arraycopy(buffer, 0, box_head, 8, ret); + byteCount += ret; + try { + mOutputStream.write(box_head, 0, v1RealDataLength); +// mOutputStream.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + + } + mOutputStream.flush(); + } + } + } catch (Exception e) { + //Crashlytics.logException(e); + //e.printStackTrace(); + //Log.e(TAG, e.getMessage()); + } + } + Log.d(TAG, getName() + ": Runner is stopped"); + } + + public void cleanup() { + mStop.set(true); + } + + public static final int CHANNEL_CMD = 22345; + + static final byte LOGIC_LINK_HEADER_0X55 = Utils.getByte(0x55); + + static final byte LOGIC_LINK_HEADER_0XCC = Utils.getByte(0xCC); +} diff --git a/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/DJIPluginRingBufferParser.java b/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/DJIPluginRingBufferParser.java new file mode 100644 index 0000000..4592ad8 --- /dev/null +++ b/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/DJIPluginRingBufferParser.java @@ -0,0 +1,195 @@ +package com.dji.wsbridge.lib; + +import android.util.Log; +import java.io.IOException; +import java.io.OutputStream; + +public class DJIPluginRingBufferParser { + private String TAG = getClass().getSimpleName(); + + /** 命令通道 */ + public static final int CHANNEL_CMD = 22345; + + static final byte LOGIC_LINK_HEADER_0X55 = Utils.getByte(0x55); + + static final byte LOGIC_LINK_HEADER_0XCC = Utils.getByte(0xCC); + + /** + * mavice air 2新增的逻辑链路的帧头长度 + */ + private static final int LOGIC_LINK_HEAD_LENGTH = 8; + + private final float factor = 1f / 8f; // 扩容因子 + + private boolean isGettedHeader = false; + private final boolean isDebug = true; + + protected byte[] buffer; + private int byteOffset = 0; + + private String name = "default"; + + private OutputStream mOutputStream; + + public DJIPluginRingBufferParser(int bufferSize, OutputStream out) { + this.buffer = new byte[bufferSize]; + mOutputStream = out; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + public void parse(byte[] buffer, int offset, int count) { + if (count > this.buffer.length - byteOffset) { + tryToExpandCapacity(count); + } + System.arraycopy(buffer, offset, this.buffer, byteOffset, count); + byteOffset += count; + findPack(); + } + + /** + * @Description : 当容量不足时,尝试扩容 + * @param length 需要扩容的容量值 + */ + private void tryToExpandCapacity(final int length) { + final int newCapacity = newCapacity(this.buffer.length, length); + if (isDebug) { + Log.d(TAG, "Try to expand capacity:" + (newCapacity - this.buffer.length)); + } + final byte[] newArray = new byte[newCapacity]; + System.arraycopy(this.buffer, 0, newArray, 0, this.buffer.length); + this.buffer = newArray; + } + + /** + * @Description : 计算新的容量值 + * @param originLength 当前的容量值 + * @param length 需要增加的容量值 + * @return + */ + private int newCapacity(final int originLength, final int length) { + int size = 0; + do { + size += (int) (factor * originLength); + } while (size <= length); + return (size + originLength); + } + + private void resetBuffer() { + if (isDebug) { + Log.d(TAG, "_" + name + "byteOffset=" + byteOffset + " expendSize=" + expendSize); + } + if (expendSize > 0) { + byteOffset -= expendSize; + if (byteOffset > 0) { + System.arraycopy(buffer, expendSize, buffer, 0, byteOffset); + } else { + if (isDebug && byteOffset < 0) { + Log.d(TAG, "_" + name + "byteOffset < 0"); + } + byteOffset = 0; + } + expendSize = 0; + } + } + + private int myHeaderIndex = 0; + private int expendSize = 0; + + private void findPack() { + + while (true) { + resetBuffer(); + + if (!isGettedHeader) { + for (int i = 0; i < byteOffset; i++) { + + if (buffer[i]==LOGIC_LINK_HEADER_0X55) { + + myHeaderIndex = i; + if (myHeaderIndex >= byteOffset) {//不足以找到完整包头 跳出循环继续接收 + expendSize = myHeaderIndex; + break; + } + + isGettedHeader = true; + + if (isGettedHeader) { + expendSize = myHeaderIndex+1; + break; + } + } + expendSize++; + }//for + } + + //没有包头 清空当前数据 + if (!isGettedHeader) { + if (isDebug) { + Log.d(TAG, "_" + name + "parseRcvBuffer 当前buffer没有包头"); + } + break; + } + + if (byteOffset < expendSize + 2) { + break; + } + + int v1Length = Utils.getInt(buffer, expendSize, 2) & 0x03FF; + int v1Version = Utils.getInt(buffer, expendSize, 2) >> 10; + if (isDebug) { + byte[] data11 = new byte[2]; + System.arraycopy(buffer, expendSize, data11, 0, 2); + Log.d(TAG, "_" + name + "parseRcvBuffer V1 data length:"+v1Length + " v1 version:" + v1Version + " "+ Utils.bytesToHex(data11)); + } + + if (v1Length < 0) { + isGettedHeader = false; + continue; + } + + if (byteOffset < myHeaderIndex + v1Length) { + break; + } + + int v1RealDataLength = v1Length + LOGIC_LINK_HEAD_LENGTH; + + PackBufferObject bufferObject = PackBufferObject.getPackBufferObject(v1RealDataLength); + byte[] box_head = bufferObject.getBuffer(); + int it = 0; + + //逻辑链路固定的header 0x55, 0xcc + box_head[it] = LOGIC_LINK_HEADER_0X55;it++; + box_head[it] = LOGIC_LINK_HEADER_0XCC;it++; + + //通道号 + box_head[it] = (byte) (CHANNEL_CMD & 0xff);it++; + box_head[it] = (byte) ((CHANNEL_CMD & 0xff00) >> 8);it++; + + //v1数据长度 + box_head[it] = (byte) (v1Length & 0xff);it++; + box_head[it] = (byte) ((v1Length & 0xff00) >> 8);it++; + box_head[it] = (byte) ((v1Length & 0xff0000) >> 16);it++; + box_head[it] = (byte) ((v1Length & 0xff000000) >> 24);it++; + + System.arraycopy(buffer, myHeaderIndex, box_head, it, v1Length); + try { + mOutputStream.write(box_head, 0, v1RealDataLength); + mOutputStream.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + expendSize += v1Length; + isGettedHeader = false; + } + + resetBuffer(); + } + +} diff --git a/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/PackBufferObject.java b/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/PackBufferObject.java new file mode 100644 index 0000000..92fcc30 --- /dev/null +++ b/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/PackBufferObject.java @@ -0,0 +1,40 @@ +package com.dji.wsbridge.lib; + +import java.util.ArrayList; + +public class PackBufferObject { + + private byte[] buffer; + private volatile boolean isUsing = true; + private boolean isRepeat; + + private PackBufferObject(int length) { + int minLength = 100; + int mLength = length< minLength ? minLength : length; + buffer = new byte[mLength];//最长上行长度 + } + + public byte[] getBuffer() { + return buffer; + } + + public void willRepeat(boolean isRepeat) { + this.isRepeat = isRepeat; + } + public void noUsed() { + if (!isRepeat)isUsing = false; + } + + private static ArrayList list = new ArrayList<>(); + static synchronized PackBufferObject getPackBufferObject(int length) { + for (PackBufferObject object : list) { + if (!object.isUsing && object.getBuffer().length>=length) { + object.isUsing = true; + return object; + } + } + PackBufferObject object = new PackBufferObject(length); + list.add(object); + return object; + } +} \ No newline at end of file diff --git a/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/Utils.java b/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/Utils.java index 156038c..5337b3b 100644 --- a/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/Utils.java +++ b/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/Utils.java @@ -200,4 +200,45 @@ public static boolean isInternalVersion() { // FirebaseCrashlytics.getInstance().recordException(exception); // } // } + + public static byte getByte(int data) { + return (byte) (data & 0xff); + } + + public static int getInt(byte[] bytes, final int offset, int length) { + if (null == bytes) { + return 0; + } + final int bytesLen = bytes.length; + if (bytesLen == 0 || offset < 0 || bytesLen <= offset) { + return 0; + } + if (length > bytesLen - offset) { + length = bytesLen - offset; + } + + int value = 0; + for (int i = length + offset - 1; i >= offset; i--) { + value = (value << 8 | (bytes[i] & 0xff)); + } + return value; + } + + public static byte[] readBytes(byte[] source, int from, int length) { + byte[] result = new byte[length]; + System.arraycopy(source, from, result, 0, length); + /** + for (int i = 0; i < length; i++) { + result[i] = source[from + i]; + } + */ + return result; + } + + public static byte[] getBytes(short data) { + byte[] bytes = new byte[2]; + bytes[0] = (byte) (data & 0xff); + bytes[1] = (byte) ((data & 0xff00) >> 8); + return bytes; + } } \ No newline at end of file diff --git a/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/connection/USBConnectionManager.java b/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/connection/USBConnectionManager.java index fb3b572..6ac9a20 100644 --- a/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/connection/USBConnectionManager.java +++ b/android-wsbridge/app/src/main/java/com/dji/wsbridge/lib/connection/USBConnectionManager.java @@ -46,6 +46,57 @@ public static USBConnectionManager getInstance() { private InputStream mInStream; private OutputStream mOutStream; + + private UsbModel currentModel = UsbModel.UNKNOWN; + + public enum UsbModel { + /** + * 231之前的整机 + */ + AG("AG410"), + + WM160("WM160"), + + /** + * 新增的逻辑链路 + */ + LOGIC_LINK("com.dji.logiclink"), + + UNKNOWN("Unknown"); + + private String value; + + UsbModel(String value) { + this.value = value; + } + + public static UsbModel find(String modelName) { + UsbModel result = UNKNOWN; + if (TextUtils.isEmpty(modelName)) { + return result; + } + + for (int i = 0; i < values().length; i++) { + if (values()[i].value.equals(modelName)) { + result = values()[i]; + break; + } + } + return result; + } + + /** + * Retrieves the display name of an enum constant. + * + * @return string The display name of an enum + */ + public String getModel() { + return this.value; + } + + } + + private BroadcastReceiver mUsbReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); @@ -111,9 +162,11 @@ public void checkForDJIAccessory() { && accessoryList.length > 0 && !TextUtils.isEmpty(accessoryList[0].getManufacturer()) && accessoryList[0].getManufacturer().equals("DJI")) { + mAccessory = accessoryList[0]; + String model = mAccessory.getModel(); + currentModel = UsbModel.find(model); BridgeApplication.getInstance().getBus().post(new RCConnectionEvent(true)); //Check permission - mAccessory = accessoryList[0]; if (mUsbManager.hasPermission(mAccessory)) { Log.d(TAG, "RC CONNECTED"); } else { @@ -179,6 +232,10 @@ public void closeStreams() { } } + public UsbModel getUSBModel() { + return currentModel; + } + /** * Event to send changes in USB Connection */ diff --git a/android-wsbridge/app/src/main/res/values/strings.xml b/android-wsbridge/app/src/main/res/values/strings.xml index 991232c..66963db 100644 --- a/android-wsbridge/app/src/main/res/values/strings.xml +++ b/android-wsbridge/app/src/main/res/values/strings.xml @@ -4,6 +4,8 @@ DJI Bridge 192.168.1.1 Settings + http://hai.wtf/bridgeapp.apk + http://hai.wtf/getBridgeVersion.php com.dji.bridgeapp.updateavailable autoupdatekey updateuri diff --git a/android-wsbridge/app/src/main/res/xml/accessory_filter.xml b/android-wsbridge/app/src/main/res/xml/accessory_filter.xml index 1074c56..50f61a6 100644 --- a/android-wsbridge/app/src/main/res/xml/accessory_filter.xml +++ b/android-wsbridge/app/src/main/res/xml/accessory_filter.xml @@ -2,4 +2,6 @@ + + \ No newline at end of file diff --git a/android-wsbridge/gradle.properties b/android-wsbridge/gradle.properties index c79c847..af6dcbe 100644 --- a/android-wsbridge/gradle.properties +++ b/android-wsbridge/gradle.properties @@ -15,4 +15,5 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file