diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..32522c1 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0ad17cb --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..1337178 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,42 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace 'com.example.epaperuploader' + compileSdk 34 + + defaultConfig { + applicationId "com.example.epaperuploader" + minSdk 24 + targetSdk 34 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + + implementation libs.appcompat + implementation libs.material + implementation libs.activity + implementation libs.constraintlayout + testImplementation libs.junit + androidTestImplementation libs.ext.junit + androidTestImplementation libs.espresso.core + implementation 'com.google.code.gson:gson:2.10.1' + implementation("com.vanniktech:android-image-cropper:4.6.0") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/epaperuploader/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/epaperuploader/ExampleInstrumentedTest.java new file mode 100644 index 0000000..dacf005 --- /dev/null +++ b/app/src/androidTest/java/com/example/epaperuploader/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.epaperuploader; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.epaperuploader", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5e60f97 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/epaperuploader/BLEDevice.java b/app/src/main/java/com/example/epaperuploader/BLEDevice.java new file mode 100644 index 0000000..8e4c1d6 --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/BLEDevice.java @@ -0,0 +1,35 @@ +package com.example.epaperuploader; + +import android.bluetooth.BluetoothDevice; + +/** + * 作者:yeqianyun on 2019/11/6 17:22 + * 邮箱:1612706976@qq.com + * + * BLE蓝牙设备 + */ +public class BLEDevice { + private BluetoothDevice bluetoothDevice; //蓝牙设备 + private int RSSI; //蓝牙信号 + + public BLEDevice(BluetoothDevice bluetoothDevice, int RSSI) { + this.bluetoothDevice = bluetoothDevice; + this.RSSI = RSSI; + } + + public BluetoothDevice getBluetoothDevice() { + return bluetoothDevice; + } + + public void setBluetoothDevice(BluetoothDevice bluetoothDevice) { + this.bluetoothDevice = bluetoothDevice; + } + + public int getRSSI() { + return RSSI; + } + + public void setRSSI(int RSSI) { + this.RSSI = RSSI; + } +} diff --git a/app/src/main/java/com/example/epaperuploader/BluetoothLeService.java b/app/src/main/java/com/example/epaperuploader/BluetoothLeService.java new file mode 100644 index 0000000..03f68d0 --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/BluetoothLeService.java @@ -0,0 +1,362 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.epaperuploader; + +import android.annotation.SuppressLint; +import android.app.Service; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCallback; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattDescriptor; +import android.bluetooth.BluetoothGattService; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.content.Intent; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +import java.util.List; +import java.util.UUID; + +/** + * Service for managing connection and data communication with a GATT server hosted on a + * given Bluetooth LE device. + */ +public class BluetoothLeService extends Service { + private final static String TAG = BluetoothLeService.class.getSimpleName(); + + private BluetoothManager mBluetoothManager; + private BluetoothAdapter mBluetoothAdapter; + private String mBluetoothDeviceAddress; + public static BluetoothGatt mBluetoothGatt; + private int mConnectionState = STATE_DISCONNECTED; + + private static final int STATE_DISCONNECTED = 0; + private static final int STATE_CONNECTING = 1; + private static final int STATE_CONNECTED = 2; + + public final static String ACTION_GATT_CONNECTED = + "ACTION_GATT_CONNECTED"; + public final static String ACTION_GATT_DISCONNECTED = + "ACTION_GATT_DISCONNECTED"; + public final static String ACTION_GATT_SERVICES_DISCOVERED = + "ACTION_GATT_SERVICES_DISCOVERED"; + public final static String ACTION_DATA_AVAILABLE = + "ACTION_DATA_AVAILABLE"; + public final static String EXTRA_DATA = + "EXTRA_DATA"; + + public final static UUID UUID_HEART_RATE_MEASUREMENT = + UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT); + + // Implements callback methods for GATT events that the app cares about. For example, + // connection change and services discovered. + private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { + @SuppressLint("MissingPermission") + @Override + public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { + String intentAction; + if (newState == BluetoothProfile.STATE_CONNECTED) { + intentAction = ACTION_GATT_CONNECTED; + mConnectionState = STATE_CONNECTED; + broadcastUpdate(intentAction); + Log.i(TAG, "Connected to GATT server."); + // Attempts to discover services after successful connection. + Log.i(TAG, "Attempting to start service discovery:" + + mBluetoothGatt.discoverServices()); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + gatt.readPhy(); + } + + } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { + intentAction = ACTION_GATT_DISCONNECTED; + mConnectionState = STATE_DISCONNECTED; + Log.i(TAG, "Disconnected from GATT server."); + broadcastUpdate(intentAction); + } + } + + @Override + public void onServicesDiscovered(BluetoothGatt gatt, int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED); + } else { + Log.w(TAG, "onServicesDiscovered received: " + status); + } + } + + @Override + public void onCharacteristicRead(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic, + int status) { + if (status == BluetoothGatt.GATT_SUCCESS) { + broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); + } + } + + @Override + public void onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); + } + + @Override + public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { + super.onCharacteristicWrite(gatt, characteristic, status); + MainActivity.handlerBt.handleUploadingStage(); + } + + @Override + public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status) { + super.onPhyRead(gatt, txPhy, rxPhy, status); + Log.e(TAG, "onPhyRead: txPhy:" + txPhy + ", rxPhy:" + rxPhy); + gatt.setPreferredPhy(BluetoothDevice.PHY_LE_2M, BluetoothDevice.PHY_LE_2M, + BluetoothDevice.PHY_OPTION_NO_PREFERRED); + } + + @Override + public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status) { + super.onPhyUpdate(gatt, txPhy, rxPhy, status); + Log.e(TAG, "onPhyRead: txPhy:" + txPhy + ", rxPhy:" + rxPhy); + } + }; + + private void broadcastUpdate(final String action) { + final Intent intent = new Intent(action); + sendBroadcast(intent); + } + + private void broadcastUpdate(final String action, + final BluetoothGattCharacteristic characteristic) { + final Intent intent = new Intent(action); + + // This is special handling for the Heart Rate Measurement profile. Data parsing is + // carried out as per profile specifications: + // http://developer.bluetooth.org/gatt/characteristics/Pages/CharacteristicViewer.aspx?u=org.bluetooth.characteristic.heart_rate_measurement.xml + if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) { + int flag = characteristic.getProperties(); + int format = -1; + if ((flag & 0x01) != 0) { + format = BluetoothGattCharacteristic.FORMAT_UINT16; + Log.d(TAG, "Heart rate format UINT16."); + } else { + format = BluetoothGattCharacteristic.FORMAT_UINT8; + Log.d(TAG, "Heart rate format UINT8."); + } + final int heartRate = characteristic.getIntValue(format, 1); + Log.d(TAG, String.format("Received heart rate: %d", heartRate)); + intent.putExtra(EXTRA_DATA, String.valueOf(heartRate)); + } else { + // For all other profiles, writes the data formatted in HEX. + final byte[] data = characteristic.getValue(); + if (data != null && data.length > 0) { + final StringBuilder stringBuilder = new StringBuilder(data.length); + for(byte byteChar : data) + stringBuilder.append(String.format("%02X ", byteChar)); + intent.putExtra(EXTRA_DATA, new String(data) + "\n" + stringBuilder.toString()); + } + } + sendBroadcast(intent); + } + + public class LocalBinder extends Binder { + BluetoothLeService getService() { + return BluetoothLeService.this; + } + } + + @Override + public IBinder onBind(Intent intent) { + Log.e(TAG, "onBind is calling:" + mBinder); + return mBinder; + } + + @Override + public boolean onUnbind(Intent intent) { + // After using a given device, you should make sure that BluetoothGatt.close() is called + // such that resources are cleaned up properly. In this particular example, close() is + // invoked when the UI is disconnected from the Service. + close(); + return super.onUnbind(intent); + } + + private final IBinder mBinder = new LocalBinder(); + + /** + * Initializes a reference to the local Bluetooth adapter. + * + * @return Return true if the initialization is successful. + */ + public boolean initialize() { + // For API level 18 and above, get a reference to BluetoothAdapter through + // BluetoothManager. + if (mBluetoothManager == null) { + mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); + if (mBluetoothManager == null) { + Log.e(TAG, "Unable to initialize BluetoothManager."); + return false; + } + } + + mBluetoothAdapter = mBluetoothManager.getAdapter(); + if (mBluetoothAdapter == null) { + Log.e(TAG, "Unable to obtain a BluetoothAdapter."); + return false; + } + + return true; + } + + /** + * Connects to the GATT server hosted on the Bluetooth LE device. + * + * @param address The device address of the destination device. + * + * @return Return true if the connection is initiated successfully. The connection result + * is reported asynchronously through the + * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)} + * callback. + */ + @SuppressLint("MissingPermission") + public boolean connect(final String address) { + if (mBluetoothAdapter == null || address == null) { + Log.w(TAG, "BluetoothAdapter not initialized or unspecified address."); + return false; + } + + // Previously connected device. Try to reconnect. + if (mBluetoothDeviceAddress != null && address.equals(mBluetoothDeviceAddress) + && mBluetoothGatt != null) { + Log.d(TAG, "Trying to use an existing mBluetoothGatt for connection."); + if (mBluetoothGatt.connect()) { + mConnectionState = STATE_CONNECTING; + return true; + } else { + return false; + } + } + + final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); + if (device == null) { + Log.w(TAG, "Device not found. Unable to connect."); + return false; + } + // We want to directly connect to the device, so we are setting the autoConnect + // parameter to false. + mBluetoothGatt = device.connectGatt(this, false, mGattCallback); + mBluetoothGatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH); + Log.d(TAG, "Trying to create a new connection."); + mBluetoothDeviceAddress = address; + mConnectionState = STATE_CONNECTING; + return true; + } + + /** + * Disconnects an existing connection or cancel a pending connection. The disconnection result + * is reported asynchronously through the + * {@code BluetoothGattCallback#onConnectionStateChange(android.bluetooth.BluetoothGatt, int, int)} + * callback. + */ + @SuppressLint("MissingPermission") + public void disconnect() { + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + Log.w(TAG, "BluetoothAdapter not initialized"); + return; + } + mBluetoothGatt.disconnect(); + } + + /** + * After using a given BLE device, the app must call this method to ensure resources are + * released properly. + */ + @SuppressLint("MissingPermission") + public void close() { + if (mBluetoothGatt == null) { + return; + } + mBluetoothGatt.close(); + mBluetoothGatt = null; + } + + /** + * Request a read on a given {@code BluetoothGattCharacteristic}. The read result is reported + * asynchronously through the {@code BluetoothGattCallback#onCharacteristicRead(android.bluetooth.BluetoothGatt, android.bluetooth.BluetoothGattCharacteristic, int)} + * callback. + * + * @param characteristic The characteristic to read from. + */ + @SuppressLint("MissingPermission") + public void readCharacteristic(BluetoothGattCharacteristic characteristic) { + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + Log.w(TAG, "BluetoothAdapter not initialized"); + return; + } + mBluetoothGatt.readCharacteristic(characteristic); + } + + @SuppressLint("MissingPermission") + public void writeCharacteristic(BluetoothGattCharacteristic characteristic) { + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + Log.w(TAG, "BluetoothAdapter not initialized"); + return; + } + mBluetoothGatt.writeCharacteristic(characteristic); + } + + /** + * Enables or disables notification on a give characteristic. + * + * @param characteristic Characteristic to act on. + * @param enabled If true, enable notification. False otherwise. + */ + @SuppressLint("MissingPermission") + public void setCharacteristicNotification(BluetoothGattCharacteristic characteristic, + boolean enabled) { + if (mBluetoothAdapter == null || mBluetoothGatt == null) { + Log.w(TAG, "BluetoothAdapter not initialized"); + return; + } + mBluetoothGatt.setCharacteristicNotification(characteristic, enabled); + + // This is specific to Heart Rate Measurement. + if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) { + BluetoothGattDescriptor descriptor = characteristic.getDescriptor( + UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG)); + descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); + mBluetoothGatt.writeDescriptor(descriptor); + } + } + + /** + * Retrieves a list of supported GATT services on the connected device. This should be + * invoked only after {@code BluetoothGatt#discoverServices()} completes successfully. + * + * @return A {@code List} of supported services. + */ + public List getSupportedGattServices() { + if (mBluetoothGatt == null) return null; + + return mBluetoothGatt.getServices(); + } +} diff --git a/app/src/main/java/com/example/epaperuploader/CropImageActivity.java b/app/src/main/java/com/example/epaperuploader/CropImageActivity.java new file mode 100644 index 0000000..be7fa40 --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/CropImageActivity.java @@ -0,0 +1,91 @@ +package com.example.epaperuploader; + +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import com.canhub.cropper.CropImageView; +import com.example.epaperuploader.image_processing.EPaperDisplay; + +import java.io.File; +import java.io.IOException; + +public class CropImageActivity extends AppCompatActivity { + private Uri sourceUri; + private Uri saveUri; + private CropImageView vCropImageView; + private final ActivityResultLauncher activityResultLauncher = registerForActivityResult( + new ActivityResultContracts.GetContent(), + uri -> vCropImageView.setImageUriAsync(uri)); + + private Button btnRotation; + private Button btnCrop; + + private int width = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].width; + private int height = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].height; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_crop_image); + + vCropImageView = findViewById(R.id.cropImageView); + vCropImageView.setOnSetImageUriCompleteListener(new CropImageView.OnSetImageUriCompleteListener() { + @Override + public void onSetImageUriComplete(@NonNull CropImageView cropImageView, @NonNull Uri uri, @Nullable Exception e) { + sourceUri = uri; + vCropImageView.setAspectRatio(width, height); + } + }); + + vCropImageView.setOnCropImageCompleteListener(new CropImageView.OnCropImageCompleteListener() { + @Override + public void onCropImageComplete(@NonNull CropImageView cropImageView, @NonNull CropImageView.CropResult cropResult) { + Intent intent = new Intent(); + intent.putExtra("FILE_NAME", saveUri); + setResult(RESULT_OK, intent); + finish(); + } + }); + + btnRotation = findViewById(R.id.btnRotation); + btnRotation.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + vCropImageView.rotateImage(90); + vCropImageView.setAspectRatio(width, height); + } + }); + + btnCrop = findViewById(R.id.btnCrop); + btnCrop.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + try { + saveUri = Uri.fromFile(File.createTempFile("esp32_image_crop", "png")); + vCropImageView.croppedImageAsync( + Bitmap.CompressFormat.PNG, + 30, + width, + height, + CropImageView.RequestSizeOptions.NONE, + saveUri + ); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + + activityResultLauncher.launch("image/*"); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/epaperuploader/DeviceScanActivity.java b/app/src/main/java/com/example/epaperuploader/DeviceScanActivity.java new file mode 100644 index 0000000..506672f --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/DeviceScanActivity.java @@ -0,0 +1,344 @@ +package com.example.epaperuploader; + +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Activity for scanning and displaying available Bluetooth LE devices. + */ +//public class DeviceScanActivity extends ListActivity { +public class DeviceScanActivity extends AppCompatActivity { + private LeDeviceListAdapter mLeDeviceListAdapter; + private BluetoothAdapter mBluetoothAdapter; + private boolean mScanning; + private Handler mHandler; + + private ListView deviceList; + + private static final int REQUEST_ENABLE_BT = 1; + // Stops scanning after 10 seconds. + private static final long SCAN_PERIOD = 10000; + + /** + * This block is for requesting permissions up to Android 12+ + * + */ + + private static final int PERMISSIONS_REQUEST_CODE = 191; + private static final String[] BLE_PERMISSIONS = new String[]{ + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION, + }; + @SuppressLint("InlinedApi") + private static final String[] ANDROID_12_BLE_PERMISSIONS = new String[]{ + Manifest.permission.BLUETOOTH_SCAN, + Manifest.permission.BLUETOOTH_CONNECT, + Manifest.permission.BLUETOOTH_ADVERTISE, + Manifest.permission.ACCESS_FINE_LOCATION + }; + + public static void requestBlePermissions(Activity activity, int requestCode) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + ActivityCompat.requestPermissions(activity, ANDROID_12_BLE_PERMISSIONS, requestCode); + else + ActivityCompat.requestPermissions(activity, BLE_PERMISSIONS, requestCode); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_device_scan); + getSupportActionBar().setTitle(R.string.title_devices); + mHandler = new Handler(); + + requestBlePermissions(this, PERMISSIONS_REQUEST_CODE); + + // Use this check to determine whether BLE is supported on the device. Then you can + // selectively disable BLE-related features. + if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { + Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show(); + finish(); + } + + // Initializes a Bluetooth adapter. For API level 18 and above, get a reference to + // BluetoothAdapter through BluetoothManager. + final BluetoothManager bluetoothManager = + (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); + mBluetoothAdapter = bluetoothManager.getAdapter(); + + // Checks if Bluetooth is supported on the device. + if (mBluetoothAdapter == null) { + Toast.makeText(this, R.string.error_bluetooth_not_supported, Toast.LENGTH_SHORT).show(); + finish(); + return; + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main, menu); + if (!mScanning) { + menu.findItem(R.id.menu_stop).setVisible(false); + menu.findItem(R.id.menu_scan).setVisible(true); + } else { + menu.findItem(R.id.menu_stop).setVisible(true); + menu.findItem(R.id.menu_scan).setVisible(false); + } + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int itemId = item.getItemId(); + if (itemId == R.id.menu_scan) { + mLeDeviceListAdapter.clear(); + scanLeDevice(true); + } else if (itemId == R.id.menu_stop) { + scanLeDevice(false); + } + + return true; + } + + @SuppressLint("MissingPermission") + @Override + protected void onResume() { + super.onResume(); + + // Ensures Bluetooth is enabled on the device. If Bluetooth is not currently enabled, + // fire an intent to display a dialog asking the user to grant permission to enable it. + if (!mBluetoothAdapter.isEnabled()) { + Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); + } + + // Initializes list view adapter. + mLeDeviceListAdapter = new LeDeviceListAdapter(); + deviceList = findViewById(R.id.device_list); + deviceList.setAdapter(mLeDeviceListAdapter); + deviceList.setOnItemClickListener(new OnDevListItemClick()); + scanLeDevice(true); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + // User chose not to enable Bluetooth. + if (requestCode == REQUEST_ENABLE_BT && resultCode == Activity.RESULT_CANCELED) { + finish(); + return; + } + super.onActivityResult(requestCode, resultCode, data); + } + + @Override + protected void onPause() { + super.onPause(); + scanLeDevice(false); + mLeDeviceListAdapter.clear(); + } + + @SuppressLint("MissingPermission") + private class OnDevListItemClick implements AdapterView.OnItemClickListener + { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) + { + // 停止扫描 + if (mScanning) { + mBluetoothAdapter.stopLeScan(mLeScanCallback); + mScanning = false; + } + final BLEDevice device = mLeDeviceListAdapter.getDevice(position); + if (device == null) return; + Intent intent = new Intent(); + intent.putExtra("DEVICE", device.getBluetoothDevice()); + setResult(RESULT_OK, intent); + finish(); + } + } + + @SuppressLint("MissingPermission") + private void scanLeDevice(final boolean enable) { + if (enable) { + // Stops scanning after a pre-defined scan period. + mHandler.postDelayed(new Runnable() { + @SuppressLint("MissingPermission") + @Override + public void run() { + mScanning = false; + mBluetoothAdapter.stopLeScan(mLeScanCallback); + invalidateOptionsMenu(); + } + }, SCAN_PERIOD); + + mScanning = true; + mBluetoothAdapter.startLeScan(mLeScanCallback); + } else { + mScanning = false; + mBluetoothAdapter.stopLeScan(mLeScanCallback); + } + invalidateOptionsMenu(); + } + + // Adapter for holding devices found through scanning. + private class LeDeviceListAdapter extends BaseAdapter { + private ArrayList mLeDevices; + private LayoutInflater mInflator; + + public LeDeviceListAdapter() { + super(); + mLeDevices = new ArrayList(); + mInflator = DeviceScanActivity.this.getLayoutInflater(); + } + + public boolean isRepeat(List rawList, BLEDevice bleDevice) { + int Num = rawList.size(); + if(Num==0) + { + return false; + } + String devName = bleDevice.getBluetoothDevice().getName(); + String devAddress = bleDevice.getBluetoothDevice().getAddress(); + for(int i =0;i 0) + viewHolder.deviceName.setText(deviceName); + else + viewHolder.deviceName.setText(R.string.unknown_device); + viewHolder.deviceAddress.setText(device.getBluetoothDevice().getAddress()); + viewHolder.deviceRSSI.setText("RSSI: " + device.getRSSI()); + + return view; + } + } + + // Device scan callback. + private BluetoothAdapter.LeScanCallback mLeScanCallback = + new BluetoothAdapter.LeScanCallback() { + + @Override + public void onLeScan(final BluetoothDevice device, int rssi, byte[] scanRecord) { + runOnUiThread(new Runnable() { + @Override + public void run() { + String devName = device.getName(); + if (devName != null && devName.length() > 0) { + BLEDevice bleDevice = new BLEDevice(device, rssi); + // mLeDeviceListAdapter.addDevice(device); + mLeDeviceListAdapter.addDevice(bleDevice); + mLeDeviceListAdapter.notifyDataSetChanged(); + } + } + }); + } + }; + + static class ViewHolder { + TextView deviceName; + TextView deviceAddress; + TextView deviceRSSI; + } +} diff --git a/app/src/main/java/com/example/epaperuploader/DisplaysActivity.java b/app/src/main/java/com/example/epaperuploader/DisplaysActivity.java new file mode 100644 index 0000000..e1451b6 --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/DisplaysActivity.java @@ -0,0 +1,71 @@ +package com.example.epaperuploader; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +import com.example.epaperuploader.image_processing.EPaperDisplay; + +public class DisplaysActivity extends AppCompatActivity implements AdapterView.OnItemClickListener +{ + // View of displays list + //--------------------------------------------------------- + private ListView listView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_displays); + getSupportActionBar().setTitle(R.string.disp); + + // ListView, its adapter and listener + //-------------------------------------- + listView = findViewById(R.id.displays_list); + listView.setAdapter(new DisplayListAdapter(this, EPaperDisplay.getDisplays())); + listView.setOnItemClickListener(this); + } + + public void onCancel(View view) { + onBackPressed(); + } + + @Override + public void onBackPressed() { + setResult(RESULT_CANCELED, new Intent()); + finish(); + } + + @Override + public void onItemClick(AdapterView adapterView, View view, int i, long l) { + EPaperDisplay.epdInd = i; + setResult(RESULT_OK, new Intent()); + finish(); + } + + + //--------------------------------------------------------- + // File list adapter + //--------------------------------------------------------- + private class DisplayListAdapter extends ArrayAdapter { + public DisplayListAdapter(Context context, EPaperDisplay[] array) { + // Standard one-line item layout + //------------------------------------------------- + super(context, android.R.layout.simple_list_item_1, array); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + TextView view = (TextView) super.getView(position, convertView, parent); + if (view != null) view.setText(EPaperDisplay.getDisplays()[position].title); + return view; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/epaperuploader/FilteringActivity.java b/app/src/main/java/com/example/epaperuploader/FilteringActivity.java new file mode 100644 index 0000000..2536103 --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/FilteringActivity.java @@ -0,0 +1,259 @@ +package com.example.epaperuploader; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.Window; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +import com.example.epaperuploader.image_processing.EPaperDisplay; +import com.example.epaperuploader.image_processing.EPaperPicture; + + +/** + *

Filtering activity

+ * The activity offers to select one of available image filters, + * which converts the loaded image for better pixel format + * converting required for selected display. + * + * @author Waveshare team + * @version 1.0 + * @since 8/18/2018 + */ + + +public class FilteringActivity extends AppCompatActivity +{ + // View + //------------------------------------------ + private Button button; + private TextView textView; + private ImageView imageView; + + private Button btnLevelMono; + private Button btnLevelColor; + private Button btnDitherMono; + private Button btnDitherMono2; + private Button btnDitherColor; + private Button btnDitherColor2; + + private ImageView imgLevelMono; + private ImageView imgLevelColor; + private ImageView imgDitherMono; + private ImageView imgDitherMono2; + private ImageView imgDitherColor; + private ImageView imgDitherColor2; + + private final static String TAG = FilteringActivity.class.getSimpleName(); + + @Override + protected void onCreate(Bundle savedInstanceState) + { + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_filtering); + getSupportActionBar().setTitle(R.string.filt); + + // View + //------------------------------------------ + textView = findViewById(R.id.txt_indexed); + imageView = findViewById(R.id.img_indexed); + + btnLevelMono = findViewById(R.id.btn_wb_l); + btnLevelColor = findViewById(R.id.btn_wbrl); + btnDitherMono = findViewById(R.id.btn_wb_d); + btnDitherMono2 = findViewById(R.id.btn_wb_d2); + btnDitherColor = findViewById(R.id.btn_wbrd); + btnDitherColor2 = findViewById(R.id.btn_wbrd2); + + imgLevelMono = findViewById(R.id.img_level_mono); + imgLevelColor = findViewById(R.id.img_level_color); + imgDitherMono = findViewById(R.id.img_dither_mono); + imgDitherMono2 = findViewById(R.id.img_dither_mono2); + imgDitherColor = findViewById(R.id.img_dither_color); + imgDitherColor2 = findViewById(R.id.img_dither_color2); + + // Disable unavailable palettes + //------------------------------------------ + boolean redIsEnabled = (EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].index & 1) != 0; + + btnLevelColor.setEnabled(redIsEnabled); + btnDitherColor.setEnabled(redIsEnabled); + btnDitherColor2.setEnabled(redIsEnabled); + + // Disable unavailable palettes just for 5.65f e-Paper + //------------------------------------------ + if(EPaperDisplay.epdInd == 25 || EPaperDisplay.epdInd == 37 || EPaperDisplay.epdInd == 43 || EPaperDisplay.epdInd == 47) + { + redIsEnabled = false; + btnLevelMono.setEnabled(redIsEnabled); + btnDitherMono.setEnabled(redIsEnabled); + btnDitherMono2.setEnabled(redIsEnabled); + } + + // 通过模拟点击,来实现预览 + if (btnLevelMono.isEnabled()) { + btnLevelMono.callOnClick(); + } + if (btnLevelColor.isEnabled()) { + btnLevelColor.callOnClick(); + } + if (btnDitherMono.isEnabled()) { + btnDitherMono.callOnClick(); + } + if (btnDitherMono2.isEnabled()) { + btnDitherMono2.callOnClick(); + } + if (btnDitherColor.isEnabled()) { + btnDitherColor.callOnClick(); + } + + if (btnDitherColor2.isEnabled()) { + btnDitherColor2.callOnClick(); + } + } + + public void onImgClick(View view) { + int img_id = view.getId(); + String name = ""; + Button btn; + if (img_id == R.id.img_level_mono) { + btn = btnLevelMono; + } else if (img_id == R.id.img_level_color) { + btn = btnLevelColor; + } else if (img_id == R.id.img_dither_mono) { + btn = btnDitherMono; + } else if (img_id == R.id.img_dither_mono2) { + btn = btnDitherMono2; + } else if (img_id == R.id.img_dither_color) { + btn = btnDitherColor; + } else if (img_id == R.id.img_dither_color2) { + btn = btnDitherColor2; + } else { + return; + } + + name = btn.getText().toString(); + + Log.e("TAG", "Image id: " + img_id + ", Name: " + name); + Intent intent = new Intent(); + intent.putExtra("NAME", name); + + btn.callOnClick(); + + setResult(RESULT_OK, intent); + finish(); + } + + // Accept the selected + public void onOk(View view) + { + // If palette is not selected, then exit + //----------------------------------------------------- + if (button == null) return; + + // Close palette activity and return palette's name + //----------------------------------------------------- + Intent intent = new Intent(); + intent.putExtra("NAME", button.getText().toString()); + + setResult(RESULT_OK, intent); + finish(); + } + + public void onCancel(View view) + { + onBackPressed(); + } + + @Override + public void onBackPressed() + { + setResult(RESULT_CANCELED); + finish(); + } + + public void onButtonClick(View view) { + int btnId = view.getId(); + boolean isLvl; + boolean isRed; + boolean isEnhance = false; + if (btnId == R.id.btn_wb_l) { + isLvl = true; + isRed = false; + } else if (btnId == R.id.btn_wbrl) { + isLvl = true; + isRed = true; + } else if (btnId == R.id.btn_wb_d) { + isLvl = false; + isRed = false; + } else if (btnId == R.id.btn_wb_d2) { + isLvl = false; + isRed = false; + isEnhance = true; + } else if (btnId == R.id.btn_wbrd) { + isLvl = false; + isRed = true; + } else if (btnId == R.id.btn_wbrd2) { + isLvl = false; + isRed = true; + isEnhance = true; + } else { + Log.e(TAG, "Error id: " + btnId + ", name: " + view.getTransitionName()); + return; + } + button = (Button)view; + run(isLvl, isRed, isEnhance); + } + + public void run(boolean isLvl, boolean isRed, boolean isEnhance) + { + // Image processing + //----------------------------------------------------- + MainActivity.indTableImage = EPaperPicture.createIndexedImage(isLvl, isRed, isEnhance); + + // Image view size calculation + //----------------------------------------------------- + int size = textView.getWidth(); + + imageView.setMaxHeight(Math.min(300, size)); + imageView.setMinimumHeight(Math.min(200, size / 2)); + imageView.setImageBitmap(MainActivity.indTableImage); + + ImageView sampleImageView; + if (isLvl) { + if (isRed) { + sampleImageView = imgLevelColor; + } else { + sampleImageView = imgLevelMono; + } + } else { + if (isRed) { + if (isEnhance) { + sampleImageView = imgDitherColor2; + } else { + sampleImageView = imgDitherColor; + } + } else { + if (isEnhance) { + sampleImageView = imgDitherMono2; + } else { + sampleImageView = imgDitherMono; + } + } + } + + sampleImageView.setMaxHeight(Math.min(300, size)); + sampleImageView.setMinimumHeight(Math.min(200, size / 2)); + sampleImageView.setImageBitmap(MainActivity.indTableImage); + + // Show selected image filter + //----------------------------------------------------- + textView.setText(button.getText()); + } +} diff --git a/app/src/main/java/com/example/epaperuploader/MainActivity.java b/app/src/main/java/com/example/epaperuploader/MainActivity.java new file mode 100644 index 0000000..475ad83 --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/MainActivity.java @@ -0,0 +1,859 @@ +package com.example.epaperuploader; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGattCharacteristic; +import android.bluetooth.BluetoothGattService; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.provider.MediaStore; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.example.epaperuploader.communication.PermissionHelper; +import com.example.epaperuploader.image_processing.EPaperDisplay; +import com.google.gson.Gson; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class MainActivity extends AppCompatActivity { + // Request codes + //----------------------------- + public static final int REQ_BLUETOOTH_CONNECTION = 2; + public static final int REQ_OPEN_FILE = 3; + public static final int REQ_DISPLAY_SELECTION = 4; + public static final int REQ_PALETTE_SELECTION = 5; + public static final int REQ_UPLOADING = 6; + public static final int SPI_PIN_CONFIG = 7; + + private final static String TAG = MainActivity.class.getSimpleName(); + + // Image file name and path + //----------------------------- + public static String fileName; + public static String filePath; + + // Data + //----------------------------- + public static Bitmap originalImage; // Loaded image with original pixel format + public static Bitmap indTableImage; // Filtered image with indexed colors + + // Views + //----------------------------- + public TextView mIndexSpiConfig; + public TextView textBlue; + public TextView textLoad; + public TextView textDisp; + public TextView textFilt; + public Button button_file; + + public ImageView pictFile; // View of loaded image + public ImageView pictFilt; // View of filtered image + + private CheckBox cbIsCompress; + private static TextView tvSendInfo; + private static Button btnUploadBt; + private Button btnUploadWifi; + private static Button btnLoadDefault; + private static ProgressBar pbUploadProgress; + + private static TextView tvUploadRatio; + private TextView tvDevAddress; + private static TextView tvConnectionState; + private static TextView tvIpAddress; + private static TextView tvHostName; + + // Device + //----------------------------- + public static BluetoothDevice btDevice = null; + + public static SocketHandlerBt handlerBt = null; + public SocketHandlerWifi handlerWifi = null; + + // service + private static BluetoothLeService mBluetoothLeService = null; + + private ArrayList> mGattCharacteristics = + new ArrayList>(); + + private BluetoothGattCharacteristic mCharacterRead; + private BluetoothGattCharacteristic mCharacterWriteMsg; + private static BluetoothGattCharacteristic mCharacterWriteData; + + private static boolean mConnected = false; + private String mDeviceAddress; + + private String mEsp32IpAddress = null; + private String mEsp32HostName = null; + + // other + SharedPreferences sharedPreferences = null; + SharedPreferences.Editor config_editor; + + public static int din_pin = 14; + public static int sck_pin = 13; + public static int cs_pin = 15; + public static int dc_pin = 27; + public static int rst_pin = 26; + public static int busy_pin = 25; + + public static Context context; + + public static int UPDATE_PROGESS = 1; + public static int UPDATE_SEND_INFO = 2; + public static int CONNECT_SUCCESS = 3; + public static int CONNECT_FAILURE = 4; + public static int DISCONNECT_SUCCESS = 5; + public static int SEND_SUCCESS = 6; + public static int SEND_FAILURE = 7; + public static int RECEIVE_SUCCESS = 8; + public static int RECEIVE_FAILURE = 9; + public static int UPDATE_WIFI_PARAMS = 10; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + // Image file name (null by default) + //----------------------------------------------------- + fileName = null; + + // Image file path (external storage root by default) + //----------------------------------------------------- + filePath = Environment.getExternalStorageDirectory().getAbsolutePath(); + + mIndexSpiConfig = findViewById(R.id.index_spi_config); + + sharedPreferences = getSharedPreferences("config", Context.MODE_PRIVATE); + config_editor = sharedPreferences.edit(); + reload_config(); + + textBlue = findViewById(R.id.text_blue); + textLoad = findViewById(R.id.text_file); + textDisp = findViewById(R.id.text_disp); + textFilt = findViewById(R.id.text_filt); + + pictFile = findViewById(R.id.pict_file); + pictFilt = findViewById(R.id.pict_filt); + button_file = findViewById(R.id.Button_file); + + context = this; + // Data + //----------------------------- + originalImage = null; + indTableImage = null; + button_file.setEnabled(false); + + // View + tvDevAddress = findViewById(R.id.device_address); + // connect state + tvConnectionState = findViewById(R.id.connect_state); + // ip address + tvIpAddress = findViewById(R.id.ip_address); + // ip address + tvHostName = findViewById(R.id.host_name); + + cbIsCompress = findViewById(R.id.is_compress); + tvSendInfo = findViewById(R.id.send_info); + btnUploadBt = findViewById(R.id.btn_upload_bt); + btnUploadWifi = findViewById(R.id.btn_upload_wifi); + btnLoadDefault = findViewById(R.id.btn_load_default); + + pbUploadProgress = findViewById(R.id.upload_progress); + + //-------------------------------------- + tvUploadRatio = findViewById(R.id.upload_text); + tvUploadRatio.setText("0%"); + + // Use this check to determine whether BLE is supported on the device. Then you can + // selectively disable BLE-related features. + if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { + Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show(); + finish(); + } + + + if (ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_ADVERTISE) != PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { + Log.e(TAG, "BLUETOOTH_SCAN, BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT"); + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_ADVERTISE, Manifest.permission.BLUETOOTH_CONNECT}, + 0x3); + } + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + Log.e(TAG, "ACCESS_FINE_LOCATION"); + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, + 0x9); + } + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_ADMIN) + != PackageManager.PERMISSION_GRANTED) { + Log.e(TAG, "BLUETOOTH_ADMIN"); + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.BLUETOOTH_ADMIN}, + 0x6); + } + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { + Log.e(TAG, "WRITE_EXTERNAL_STORAGE"); + //申请WRITE_EXTERNAL_STORAGE权限 + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, + 0x3); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter(), RECEIVER_EXPORTED); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater menuInflater = getMenuInflater(); + menuInflater.inflate(R.menu.config_menu, menu); + + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onMenuOpened(int featureId, Menu menu) { + if (menu != null) { + if (menu.getClass().getSimpleName().equalsIgnoreCase("MenuBuilder")) { + try { + Method method = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE); + method.setAccessible(true); + method.invoke(menu, true); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + return super.onMenuOpened(featureId, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int item_id = item.getItemId(); + + if (item_id == R.id.spi_pin_config) { + startActivityForResult( + new Intent(this, SpiPinConfigActivity.class), + SPI_PIN_CONFIG); + } else if (item_id == R.id.network_config) { + Toast.makeText(this, "偷懒,没做^_^", Toast.LENGTH_SHORT).show(); + } else if (item_id == R.id.save_image) { + saveImage(); + } else if (item_id == R.id.reboot_device) { + rebootDevice(); + } else { + Toast.makeText(this, "unknown option:" + item_id, Toast.LENGTH_SHORT).show(); + return false; + } + + return super.onOptionsItemSelected(item); + } + + // Handles various events fired by the Service. + // ACTION_GATT_CONNECTED: connected to a GATT server. + // ACTION_GATT_DISCONNECTED: disconnected from a GATT server. + // ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services. + // ACTION_DATA_AVAILABLE: received data from the device. This can be a result of read + // or notification operations. + private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) { + updateConnectionState(R.string.connected); + invalidateOptionsMenu(); + } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) { + mConnected = false; + updateConnectionState(R.string.disconnected); + invalidateOptionsMenu(); + btDevice = null; + mDeviceAddress = null; + clearUI(); + } else if (BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED.equals(action)) { + // Show all the supported services and characteristics on the user interface. + displayGattServices(mBluetoothLeService.getSupportedGattServices()); + } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) { + displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA)); + } + } + }; + + private static IntentFilter makeGattUpdateIntentFilter() { + final IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED); + intentFilter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED); + intentFilter.addAction(BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED); + intentFilter.addAction(BluetoothLeService.ACTION_DATA_AVAILABLE); + return intentFilter; + } + + public void reload_config() { + String config_list_str = sharedPreferences.getString("config_list", ""); + // if not config spi + if (config_list_str.isEmpty()) { + // set to default + mIndexSpiConfig.setText("首次使用,请先进菜单选择SPI管脚配置 ↗"); + } else { + System.out.println(config_list_str); + try { + JSONArray jsonArray = new JSONArray(config_list_str); + JSONObject jsonObject; + for (int i = 0; i < jsonArray.length(); i++) { + jsonObject = jsonArray.getJSONObject(i); + boolean is_select = jsonObject.getBoolean("is_select"); + // skip unselected + if (!is_select) { + continue; + } + String config_name = jsonObject.optString("config_name"); + din_pin = jsonObject.getInt("din"); + sck_pin = jsonObject.getInt("sck"); + cs_pin = jsonObject.getInt("cs"); + dc_pin = jsonObject.getInt("dc"); + rst_pin = jsonObject.getInt("rst"); + busy_pin = jsonObject.getInt("busy"); + System.out.println(config_name + ":" + "din=" + din_pin + ", sck=" + sck_pin + ", cs=" + cs_pin + + ", dc=" + dc_pin + ", rst=" + rst_pin + ", busy=" + busy_pin + ", is_select=" + is_select); + + SpiPinConfigInfo configInfo = new SpiPinConfigInfo(config_name, din_pin, sck_pin, cs_pin, dc_pin, rst_pin, busy_pin, is_select); + mIndexSpiConfig.setText(configInfo.getConfigVerbose()); + return; + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + + public void onScan(View view) { + // Open bluetooth devices scanning activity + //----------------------------------------------------- + startActivityForResult( + new Intent(this, DeviceScanActivity.class), + REQ_BLUETOOTH_CONNECTION); + } + + public void onDisplay(View view) + { + // Open display selection activity + //----------------------------------------------------- + startActivityForResult( + new Intent(this, DisplaysActivity.class), + REQ_DISPLAY_SELECTION); + } + + public void onLoad(View view) + { + startActivityForResult( + new Intent(this, CropImageActivity.class), + REQ_OPEN_FILE + ); + } + + // image process + public void onFilter(View view) { + // Check if any image is loaded + //----------------------------------------------------- + if (originalImage == null) { + PermissionHelper.note(this, R.string.no_pict); + } + // Check if any display is selected + //----------------------------------------------------- + else if (EPaperDisplay.epdInd == -1) { + PermissionHelper.note(this, R.string.no_disp); + } + + // Open palette selection activity + //----------------------------------------------------- + else { + startActivityForResult( + new Intent(this, FilteringActivity.class), + REQ_PALETTE_SELECTION); + } + } + + // Code to manage Service lifecycle. + private final ServiceConnection mServiceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName componentName, IBinder service) { + mBluetoothLeService = ((BluetoothLeService.LocalBinder) service).getService(); + if (!mBluetoothLeService.initialize()) { + Log.e(TAG, "Unable to initialize Bluetooth"); + finish(); + } + Log.e(TAG, "initialize Bluetooth Le Service"); + // Automatically connects to the device upon successful start-up initialization. + mBluetoothLeService.connect(mDeviceAddress); + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + mBluetoothLeService = null; + } + }; + + private void displayData(String data) { + Log.e(TAG, "displayData:" + data); + if (data != null) { + try { + JSONObject jsonObject = new JSONObject(data); + mEsp32IpAddress = jsonObject.optString("ip_address"); + mEsp32HostName = jsonObject.optString("host_name"); + tvIpAddress.setText(mEsp32IpAddress); + tvHostName.setText(mEsp32HostName); + btnUploadWifi.setEnabled(true); + Log.e(TAG, "ip_address:" + mEsp32IpAddress + ", host_name:" + mEsp32HostName); + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + + // Demonstrates how to iterate through the supported GATT Services/Characteristics. + // In this sample, we populate the data structure that is bound to the ExpandableListView + // on the UI. + private void displayGattServices(List gattServices) { + if (gattServices == null) return; + String uuid = null; + String unknownServiceString = getResources().getString(R.string.unknown_service); + String unknownCharaString = getResources().getString(R.string.unknown_characteristic); + String serviceName = "E-Paper"; + String characterReadName = "Read WiFi IP"; + String characterWriteMsgName = "Write msg"; + String characterWriteDataName = "Write data"; + String characterName = ""; + ArrayList> gattServiceData = new ArrayList>(); + ArrayList>> gattCharacteristicData + = new ArrayList>>(); + mGattCharacteristics = new ArrayList>(); + + // Loops through available GATT Services. + // iter GATT service + for (BluetoothGattService gattService : gattServices) { + HashMap currentServiceData = new HashMap(); + // get UUID + uuid = gattService.getUuid().toString(); + // compare + if (!SampleGattAttributes.lookup(uuid, unknownServiceString).equals(serviceName)) { + continue; + } + + ArrayList> gattCharacteristicGroupData = + new ArrayList>(); + // get characteristics list + List gattCharacteristics = + gattService.getCharacteristics(); + ArrayList charas = + new ArrayList(); + + // Loops through available Characteristics. + for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) { + charas.add(gattCharacteristic); + HashMap currentCharaData = new HashMap(); + // get & compare UUID + uuid = gattCharacteristic.getUuid().toString(); + characterName = SampleGattAttributes.lookup(uuid, unknownCharaString); + if (characterName.equals(characterReadName)) { + mCharacterRead = gattCharacteristic; + mBluetoothLeService.readCharacteristic(gattCharacteristic); + } else if (characterName.equals(characterWriteMsgName)) { + mCharacterWriteMsg = gattCharacteristic; + } else if (characterName.equals(characterWriteDataName)) { + mCharacterWriteData = gattCharacteristic; + } else { + continue; + } + } + mGattCharacteristics.add(charas); + gattCharacteristicData.add(gattCharacteristicGroupData); + } + } + + public static void sendData(byte[] buffArr) { + mCharacterWriteData.setValue(buffArr); + mBluetoothLeService.writeCharacteristic(mCharacterWriteData); + } + + private static void updateConnectionState(final int resourceId) { + if (resourceId == R.string.connected) { + tvConnectionState.setTextColor(Color.GREEN); + mConnected = true; + btnUploadBt.setEnabled(true); + btnLoadDefault.setEnabled(true); + } else { + tvConnectionState.setTextColor(Color.RED); + mConnected = false; + btnUploadBt.setEnabled(false); + btnLoadDefault.setEnabled(false); + + // clear wifi params when disconnected + clearWifiParams(); + } + tvConnectionState.setText(resourceId); + } + + private static void clearWifiParams() { + tvIpAddress.setText(""); + tvHostName.setText(""); + } + + private static void updateWifiParams(String rcvData) { + try { + JSONObject jsonObject = new JSONObject(rcvData); + tvIpAddress.setText(jsonObject.optString("IP_ADDRESS")); + tvHostName.setText(jsonObject.optString("HOST_NAME")); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + + public void onUploadBt(View view) { + int epdInd = EPaperDisplay.epdInd; + boolean is_compress; + if (cbIsCompress.isChecked()) { + is_compress = true; + } else { + is_compress = false; + } + // keep screen on +// view.setKeepScreenOn(true); + handlerBt = new SocketHandlerBt(); + handlerBt.init_image(indTableImage, is_compress, null); + } + + public void onUploadWifi(View view) { + int epdInd = EPaperDisplay.epdInd; + int width = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].width; + int height = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].height; + + boolean is_compress; + if (cbIsCompress.isChecked()) { + is_compress = true; + } else { + is_compress = false; + } + + handlerWifi = new SocketHandlerWifi(mEsp32IpAddress, mEsp32HostName); + handlerWifi.init_image(indTableImage, is_compress); + + new Thread(new Runnable() { + @Override + public void run() { + Log.e(TAG, "http write start"); + // EPD: initial + // LOADA: send phase 1 data + // LOADB: send phase 2 data + // SHOW: display image + String uri = "EPD"; + try { + // init params include: epd index and spi pin configuration + JSONObject jsonObject = new JSONObject(); + jsonObject.put("epdInd", epdInd); + jsonObject.put("din", din_pin); + jsonObject.put("sck", sck_pin); + jsonObject.put("cs", cs_pin); + jsonObject.put("dc", dc_pin); + jsonObject.put("rst", rst_pin); + jsonObject.put("busy", busy_pin); + jsonObject.put("width", width); + jsonObject.put("height", height); + + String post_data = jsonObject.toString(); + + handlerWifi.httpWrite(uri, post_data); + + boolean is_continue = true; + while (is_continue) { + is_continue = handlerWifi.handleUploadingStage(); + } + } catch (JSONException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }).start(); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + Log.e(TAG, "requestCode:" + requestCode + ", resultCode:" + resultCode); + //----------------------------------------------------- + // Messages form ScanningActivity + //----------------------------------------------------- + if (requestCode == REQ_BLUETOOTH_CONNECTION) { + // Bluetooth device was found and selected + //------------------------------------------------- + if (resultCode == RESULT_OK) { + // Get selected bluetooth device + //--------------------------------------------- + btDevice = data.getParcelableExtra("DEVICE"); + + // Show name and address of the device + //--------------------------------------------- + textBlue.setText(btDevice.getName() + " (" + btDevice.getAddress() + ")"); + + // device address + mDeviceAddress = btDevice.getAddress(); + tvDevAddress.setText(mDeviceAddress); + + //-------------------------------------- + tvUploadRatio.setText("0%"); + + Intent gattServiceIntent = new Intent(this, BluetoothLeService.class); + boolean bindResult = bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE); + Log.e(TAG, "mServiceConnection: " + mServiceConnection + " bindResult: " + bindResult); + + if (mBluetoothLeService != null) { + final boolean result = mBluetoothLeService.connect(mDeviceAddress); + Log.d(TAG, "Connect request result=" + result); + } + } + } + + //----------------------------------------------------- + // Message form open file activity + //----------------------------------------------------- + else if (requestCode == REQ_OPEN_FILE) { + if (resultCode == RESULT_OK) { + // Getting file name and file path + //--------------------------------------------- + Uri image_uri = data.getParcelableExtra("FILE_NAME"); + textLoad.setText(image_uri.getPath()); + + // Loading of the selected file + //--------------------------------------------- + InputStream is = null; + try { + int width = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].width; + int height = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].height; + + originalImage = MediaStore.Images.Media.getBitmap(this.getContentResolver(), image_uri); + originalImage = Bitmap.createScaledBitmap(originalImage, width, height, false); + int pictWidth = originalImage.getWidth(); + int pictHeight = originalImage.getHeight(); + pictFile.setMaxHeight(Math.min(300, pictWidth)); + pictFile.setMinimumHeight(Math.min(200, pictWidth / 2)); + pictFile.setImageBitmap(originalImage); + textLoad.setText("width:" + pictWidth + ", height:" + pictHeight); + } catch (FileNotFoundException e) { + textFilt.setText(R.string.failed_file); + throw new RuntimeException(e); + } catch (IOException e) { + textFilt.setText(R.string.failed_file); + throw new RuntimeException(e); + } + } + } + + //----------------------------------------------------- + // Message form display selection activity + //----------------------------------------------------- + else if (requestCode == REQ_DISPLAY_SELECTION) { + if (resultCode == RESULT_OK) { + textDisp.setText(EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].title); + button_file.setEnabled(true); + } + } + + //----------------------------------------------------- + // Message form palette selection activity + //----------------------------------------------------- + else if (requestCode == REQ_PALETTE_SELECTION) { + if (resultCode == RESULT_OK) { + textFilt.setText(data.getStringExtra("NAME")); + + try { + int size = pictFile.getHeight(); + pictFilt.setMaxHeight(Math.min(300, size)); + pictFilt.setMinimumHeight(Math.min(200, size / 2)); + pictFilt.setImageBitmap(indTableImage); + } catch (Exception e) { + textFilt.setText(R.string.failed_filt); + } + } + } else if (requestCode == SPI_PIN_CONFIG) { + if (resultCode == RESULT_OK) { + reload_config(); + } + } + } + + private void clearUI() { + tvDevAddress.setText(""); + tvIpAddress.setText(""); + tvHostName.setText(""); + } + + @SuppressLint("HandlerLeak") + public static Handler mUIHandler = new Handler(){ + @Override + public void handleMessage(@NonNull Message msg) { + super.handleMessage(msg); + if (msg.what == UPDATE_PROGESS) { + tvUploadRatio.setText(msg.arg1 + "%"); + pbUploadProgress.setProgress(msg.arg1); + } else if (msg.what == UPDATE_SEND_INFO) { + tvSendInfo.setText((String)(msg.obj)); + } else if (msg.what == CONNECT_SUCCESS) { + updateConnectionState(R.string.connected); + } else if (msg.what == CONNECT_FAILURE) { + updateConnectionState(R.string.connect_fail); + } else if (msg.what == DISCONNECT_SUCCESS) { + updateConnectionState(R.string.disconnected); + } else if (msg.what == UPDATE_WIFI_PARAMS) { + updateWifiParams((String)msg.obj); + } + } + }; + + public static void updateProgress(int progress) { + new Thread(new Runnable() { + @Override + public void run() { + Message msg = new Message(); + msg.what = UPDATE_PROGESS; + msg.arg1 = progress; + mUIHandler.sendMessage(msg); + } + }).start(); + } + + + public static void updateSendInfo(String info) { + new Thread(new Runnable() { + @Override + public void run() { + Message msg = new Message(); + msg.what = UPDATE_SEND_INFO; + msg.obj = info; + mUIHandler.sendMessage(msg); + } + }).start(); + } + + + public void saveImage() { + // Check if any image is loaded + //----------------------------------------------------- + if (originalImage == null) { + PermissionHelper.note(this, R.string.no_pict); + } + // Check if any display is selected + //----------------------------------------------------- + else if (EPaperDisplay.epdInd == -1) { + PermissionHelper.note(this, R.string.no_disp); + } + else if (indTableImage == null) { + PermissionHelper.note(this, R.string.no_filt); + } else { + int h = indTableImage.getHeight(); + int w = indTableImage.getWidth(); + int[] saved_image_data = new int[w*h]; + int i = 0; + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++, i++) + saved_image_data[i] = indTableImage.getPixel(x, y); + Gson gson = new Gson(); + String saved_image_str = gson.toJson(saved_image_data); + config_editor.putString("saved_image_" + EPaperDisplay.epdInd, saved_image_str); + config_editor.commit(); + Toast.makeText(this, "保存成功", Toast.LENGTH_SHORT).show(); + } + } + + public void loadDefault(View view) { + if (EPaperDisplay.epdInd == -1) { + PermissionHelper.note(this, R.string.no_disp); + return; + } + + String saved_image_str = sharedPreferences.getString("saved_image_" + EPaperDisplay.epdInd, ""); + if (saved_image_str.isEmpty()) { + Toast.makeText(this, "没有找到图像,请先保存", Toast.LENGTH_SHORT).show(); + return; + } + Gson gson = new Gson(); + int[] saved_image_data = gson.fromJson(saved_image_str, int [].class); + Toast.makeText(this, "像素数:" + saved_image_data.length, Toast.LENGTH_SHORT).show(); + System.out.println(saved_image_data.length); + + boolean is_compress = cbIsCompress.isChecked(); + // keep screen on +// view.setKeepScreenOn(true); + handlerBt = new SocketHandlerBt(); + handlerBt.init_image(MainActivity.indTableImage, is_compress, saved_image_data); + } + + public void rebootDevice() { + if (btDevice == null) { + Toast.makeText(this, "蓝牙设备未连接", Toast.LENGTH_SHORT).show(); + return; + } + + Toast.makeText(this, "设备重启中...", Toast.LENGTH_SHORT).show(); + + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("COMMAND", "reboot_device"); + byte[] send_bytes = jsonObject.toString().getBytes(StandardCharsets.UTF_8); + sendData(send_bytes); + } catch (JSONException e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/epaperuploader/SampleGattAttributes.java b/app/src/main/java/com/example/epaperuploader/SampleGattAttributes.java new file mode 100644 index 0000000..386d9d2 --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/SampleGattAttributes.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.epaperuploader; + +import java.util.HashMap; + +/** + * This class includes a small subset of standard GATT attributes for demonstration purposes. + */ +public class SampleGattAttributes { + private static HashMap attributes = new HashMap(); + public static String HEART_RATE_MEASUREMENT = "00002a37-0000-1000-8000-00805f9b34fb"; + public static String CLIENT_CHARACTERISTIC_CONFIG = "00002902-0000-1000-8000-00805f9b34fb"; + + static { + // Sample Services. + attributes.put("0000180d-0000-1000-8000-00805f9b34fb", "Heart Rate Service"); + attributes.put("0000180a-0000-1000-8000-00805f9b34fb", "Device Information Service"); + // Sample Characteristics. + attributes.put(HEART_RATE_MEASUREMENT, "Heart Rate Measurement"); + attributes.put("00002a29-0000-1000-8000-00805f9b34fb", "Manufacturer Name String"); + // 增加墨水屏的UUID定制 + attributes.put("6e400001-b5a3-f393-e0a9-e50e24dcca9e", "E-Paper"); + attributes.put("6e400003-b5a3-f393-e0a9-e50e24dcca9e", "Read WiFi IP"); + attributes.put("6e400004-b5a3-f393-e0a9-e50e24dcca9e", "Write msg"); + attributes.put("6e400005-b5a3-f393-e0a9-e50e24dcca9e", "Write data"); + + } + + public static String lookup(String uuid, String defaultName) { + String name = attributes.get(uuid); + return name == null ? defaultName : name; + } +} diff --git a/app/src/main/java/com/example/epaperuploader/SocketHandlerBt.java b/app/src/main/java/com/example/epaperuploader/SocketHandlerBt.java new file mode 100644 index 0000000..3c746d5 --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/SocketHandlerBt.java @@ -0,0 +1,622 @@ +package com.example.epaperuploader; + +import static com.example.epaperuploader.MainActivity.busy_pin; +import static com.example.epaperuploader.MainActivity.cs_pin; +import static com.example.epaperuploader.MainActivity.dc_pin; +import static com.example.epaperuploader.MainActivity.din_pin; +import static com.example.epaperuploader.MainActivity.rst_pin; +import static com.example.epaperuploader.MainActivity.sck_pin; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.os.Handler; +import android.util.Log; + +import com.example.epaperuploader.image_processing.EPaperDisplay; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +//--------------------------------------------------------- +// Socket Handler +//--------------------------------------------------------- +class SocketHandlerBt extends Handler +{ + + // Uploaded data buffer + //--------------------------------------------------------- + private static final int BUFF_SIZE = 512; + private static final int BUFF_SIZE_WIFI = 496; + private static byte[] buffArr = new byte[BUFF_SIZE]; + private static int buffInd; + private static int xLine; + private static String rqMsg; + + private int pxInd; // Pixel index in picture + private int stInd; // Stage index of uploading + private int dSize; // Size of uploaded data by LOAD command + private int[] array; // Values of picture pixels + + private int sendSize; + private boolean next_stage; // 是否分两批发送: 双色或七色fasle,三色true + private boolean load_stage = false; + private boolean mIsCompress = false; + private byte[] compress_data; + private byte[] compress_data2; + + private final static String TAG = SocketHandlerBt.class.getSimpleName(); + + public SocketHandlerBt() + { + super(); + } + + // 图像数据压缩 + byte[] compress_image_data(byte[] image_array) { + // 输出字节流 + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + // GZip输出流 + GZIPOutputStream gzipOutputStream; + try { + // 将图像数据导入GZip + gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream); + gzipOutputStream.write(image_array); + // 关闭GZip输出流 + gzipOutputStream.close(); + // 记录压缩后的字节流 + byte[] encode_array = byteArrayOutputStream.toByteArray(); + // 释放输出字节流 + byteArrayOutputStream.flush(); + byteArrayOutputStream.close(); + System.out.println("gzip compress len=" + encode_array.length); + // 打印压缩后的数据 + return encode_array; + } catch (IOException e) { + System.out.println("gzip compress error."+e); + return null; + } + } + + // 解压缩图像数据 + byte[] decompress_image_data(byte[] encode_array) { + try { + // 定义临时缓冲区 + byte[] buffer = new byte[1024]; + byte[] image_data; + + // 输出字节流 + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + // 输入字节流 + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(encode_array); + // GZip输入流 + GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream); + // 批量写入输出字节流 + int n = 0; + while ((n = gzipInputStream.read(buffer, 0, buffer.length)) > 0) { + outputStream.write(buffer, 0, n); + } + + // 打印解压后的数据 + System.out.println("gzip decompress len=" + outputStream.toByteArray().length); + + image_data = outputStream.toByteArray(); + // 释放流资源 + outputStream.close(); + byteArrayInputStream.close(); + gzipInputStream.close(); + return image_data; + } catch (IOException e) { + System.out.println("gzip decompress error."+e); + return null; + } + } + + private byte[] get_compress_data(int epdInd, int c) { + byte[] byteArr; + int i; + int index = 0; + int pixelId = 0; + + if(epdInd == 25 || epdInd ==37 || epdInd == 43 || epdInd == 47) { + byteArr = new byte[array.length / 2]; + while (pixelId < array.length) { + + int v = 0; + // 从发送数据的角度看,每个字节高4bit和低4bit可以分别存储一个像素颜色值(0~7) + // 所以4个pixel占用2个存储单元 + for (i = 0; i < 16; i += 4) { + if (pixelId < array.length) v |= (array[pixelId] << i); + pixelId++; + } + + byteArr[index++] = (byte) (v); + byteArr[index++] = (byte) (v >> 8); + } + } else if(epdInd == 45) { + byteArr = new byte[array.length/4]; + while (pixelId < array.length) + { + int v = 0; + + for (i = 0; i < 8; i += 2) + { + if (pixelId < array.length) v |= (array[pixelId] << (6-i)); + pixelId++; + } + + byteArr[index++] = (byte)v; + } + } else { + byteArr = new byte[array.length/8]; + while (pixelId < array.length) + { + int v = 0; + + for (i = 0; i < 8; i++) + { + if ((pixelId < array.length) && (array[pixelId] != c)) v |= (128 >> i); + pixelId++; + } + + byteArr[index++] = (byte)v; + } + } + + return byteArr; + } + + // Converts picture pixels into selected pixel format + // and sends EPDx command + //----------------------------------------------------- + public boolean init_image(Bitmap bmp, boolean is_compress, int[] saved_image_data) + { + int w, h; + int epdInd = EPaperDisplay.epdInd; + + // Loading pixels into array + //------------------------------------------------- + System.out.println("=========init_image is calling =========="); + + if (saved_image_data == null) { + w = bmp.getWidth(); + h = bmp.getHeight(); + } else { + w = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].width; + h = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].height; + } + + array = new int[w*h]; // Array of pixels + int i = 0; // Index of pixel in the array of pixels + + if (saved_image_data == null) { + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++, i++) + if(epdInd == 25 || epdInd ==37 || epdInd == 43 || epdInd == 47) + array[i] = getVal_7color(bmp.getPixel(x, y)); + else + array[i] = getVal(bmp.getPixel(x, y)); + } else { + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++, i++) + if(epdInd == 25 || epdInd ==37 || epdInd == 43 || epdInd == 47) + array[i] = getVal_7color(saved_image_data[i]); + else + array[i] = getVal(saved_image_data[i]); + } + + byte[] byteArr; +// byte[] decompressed_data; + + System.out.println("image array size: " + array.length); + + mIsCompress = is_compress; + + if (mIsCompress) + { + // re-init + compress_data = null; + compress_data2 = null; + + // 数据压缩 + int c = 0; + // 先根据不同屏幕,获取编码数据 + byteArr = get_compress_data(epdInd, c); + // 再对编码结果进行压缩 + compress_data = compress_image_data(byteArr); +// decompressed_data = decompress_image_data(compress_data); + + // 如果是三色屏的话,还需要再准备第二批代表红色的数据 + if ((epdInd == 3) || (epdInd == 39) || (epdInd==0)||(epdInd==6)||(epdInd==7)||(epdInd==9)||(epdInd==12)|| + (epdInd==16)||(epdInd==19)||(epdInd==22)||(epdInd==26)||(epdInd==27)||(epdInd==28)){ + // do nothing + } else if (epdInd>15 && epdInd < 22) { + // do nothing + } else if (epdInd == 25 || epdInd == 37 || epdInd == 43 || epdInd == 47) { + // do nothing + } else { + c = 3; + byteArr = get_compress_data(epdInd, c); + compress_data2 = compress_image_data(byteArr); +// decompressed_data = decompress_image_data(compress_data2); + } + } + + pxInd = 0; // pixel id + xLine = 0; //2.13inch + stInd = 0; // step id + dSize = 0; // nof data size + + sendSize = 0; // nof send size + load_stage = false; + next_stage = false; // if has next stage + +// buffInd = 9; // Size of command in bytes + buffInd = 13; // Size of command in bytes + buffArr[0] = (byte)'I'; // Name of command (Initialize) + buffArr[1] = (byte)EPaperDisplay.epdInd; // Index of display + + // 数据是否压缩 + if (mIsCompress) { + buffArr[2] = (byte)1; + } else { + buffArr[2] = (byte)0; + } + // SPI管脚信息 + buffArr[3] = (byte)din_pin; + buffArr[4] = (byte)sck_pin; + buffArr[5] = (byte)cs_pin; + buffArr[6] = (byte)dc_pin; + buffArr[7] = (byte)rst_pin; + buffArr[8] = (byte)busy_pin; + + //图像尺寸 + buffArr[9] = (byte)(EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].width >> 8); + buffArr[10] = (byte)EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].width; + buffArr[11] = (byte)(EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].height >> 8); + buffArr[12] = (byte)EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].height; + + return u_send(false); +// return true; + } + + // The function is executed after every "Ok!" response + // obtained from esp32, which means a previous command + // is complete and esp32 is ready to get the new one. + //----------------------------------------------------- + // 这个接口每调用一次,就批量发送一批数据,循环往复直到发送完毕 + public boolean handleUploadingStage() + { + int epdInd = EPaperDisplay.epdInd; + + // 2.13 e-Paper display + if ((epdInd == 3) || (epdInd == 39)) + { + if(stInd == 0) return u_line(0, 0, 100); + //------------------------------------------------- + if(stInd == 1) return u_show(); + } + + // 2.13 b V4 + else if ((epdInd == 40)) + { + if(stInd == 0) return u_line(0, 0, 50); + if(stInd == 1) return u_next(); + if(stInd == 2) return u_line(3, 50, 50); + if(stInd == 3) return u_show(); + } + + // White-black e-Paper displays + //------------------------------------------------- + else if ((epdInd==0)||(epdInd==6)||(epdInd==7)||(epdInd==9)||(epdInd==12)|| + (epdInd==16)||(epdInd==19)||(epdInd==22)||(epdInd==26)||(epdInd==27)||(epdInd==28)) + { + if(stInd == 0) return u_data(0,0,100); + if(stInd == 1) return u_show(); + } + + // 7.5 colored e-Paper displays + //------------------------------------------------- + else if ((epdInd>15 && epdInd < 22) || (epdInd == 45)) + { + if(stInd == 0) return u_data(-1,0,100); + if(stInd == 1) return u_show(); + } + + // 5.65f colored e-Paper displays + //------------------------------------------------- + else if (epdInd == 25 || epdInd == 37 || epdInd == 43 || epdInd == 47) + { + if(stInd == 0) return u_data(-2,0,100); + if(stInd == 1) return u_show(); + } + + // Other colored e-Paper displays + //------------------------------------------------- + else + { + if(stInd == 0)return u_data((epdInd == 1)? -1 : 0,0,50); + if(stInd == 1)return u_next(); + if(stInd == 2)return u_data(3,50,50); + if(stInd == 3)return u_show(); + } + + return true; + } + + // Returns the index of color in palette + //----------------------------------------------------- + public int getVal(int color) + { + int r = Color.red(color); + int g = Color.green(color); + int b = Color.blue(color); + +// 黑色:对应00b +// 白色:对应01b +// 黄色:对应10b +// 红色:对应11b +// @ColorInt public static final int BLACK = 0xFF000000; +// @ColorInt public static final int WHITE = 0xFFFFFFFF; +// @ColorInt public static final int RED = 0xFFFF0000; +// @ColorInt public static final int YELLOW = 0xFFFFFF00; + if((r == 0xFF) && (b == 0xFF)) return 1; + if((r == 0x7F) && (b == 0x7F)) return 2; + if((r == 0xFF) && (g == 0xFF)) return 2; + if((r == 0xFF) && (b == 0x00)) return 3; + + return 0; + } + + // Returns the index of color in palette just for 5.65f e-Paper + //----------------------------------------------------- + public int getVal_7color(int color) + { + int r = Color.red(color); + int g = Color.green(color); + int b = Color.blue(color); + + if((r == 0x00) && (g == 0x00) && (b == 0x00)) return 0; + if((r == 0xFF) && (g == 0xFF) && (b == 0xFF)) return 1; + if((r == 0x00) && (g == 0xFF) && (b == 0x00)) return 2; + if((r == 0x00) && (g == 0x00) && (b == 0xFF)) return 3; + if((r == 0xFF) && (g == 0x00) && (b == 0x00)) return 4; + if((r == 0xFF) && (g == 0xFF) && (b == 0x00)) return 5; + if((r == 0xFF) && (g == 0x80) && (b == 0x00)) return 6; + + return 7; + } + + // Sends command cmd + //----------------------------------------------------- + private boolean u_send(boolean next) + { + byte[] sendArr = new byte[buffInd]; + System.arraycopy(buffArr, 0, sendArr, 0, buffInd); + MainActivity.sendData(sendArr); + + if(next) stInd++; // Go to next stage if it is needed + return true; // Command is sent successful + } + + // Next stage command + //----------------------------------------------------- + private boolean u_next() + { + Log.e(TAG, "u_next: " + stInd); + buffInd = 1; // Size of command in bytes + buffArr[0] = (byte)'N';// Name of command (Next) + + pxInd = 0; + load_stage = false; + next_stage = true; + return u_send(true); + } + + // The finishing command + //----------------------------------------------------- + private boolean u_show() + { + buffInd = 1; // Size of command in bytes + buffArr[0] = (byte)'S';// Name of command (Show picture) + + // Return false if the SHOW command is not sent + //------------------------------------------------- + if (!u_send(true)) return false; + + // Otherwise exit the uploading activity. + //------------------------------------------------- + return true; + } + + // Sends pixels of picture and shows uploading progress + //----------------------------------------------------- + private boolean u_load(int k1, int k2) + { + // Uploading progress message + //------------------------------------------------- + // 计算出已发送的数据占全部数据的百分比 + // pxInd=1830, x=0+100*1830/122*250=6% + int progress = 0; + if (mIsCompress) { + // 两批压缩的数据长度可能会不一样,但是进度暂时按各50%统计 + if (!next_stage) { + progress = (k1 + k2*pxInd/compress_data.length); + } else { + progress = (k1 + k2*pxInd/compress_data2.length); + } + } else { + progress = (k1 + k2*pxInd/array.length); + } + String x = "" + progress; + + if (x.length() > 5) x = x.substring(0, 5); + MainActivity.updateProgress(progress); + + // Size of uploaded data + //------------------------------------------------- + dSize += buffInd; + sendSize += buffInd; + + String send_info; + if (mIsCompress) { + if (next_stage) { + send_info = "像素:" + array.length + ",发送字节:" + sendSize + "/" + (compress_data.length + compress_data2.length); + } else { + send_info = "像素:" + array.length + ",发送字节:" + sendSize + "/" + compress_data.length; + } + } else { + send_info = "像素:" + array.length + ",发送字节:" + sendSize; + } + MainActivity.updateSendInfo(send_info); + + if (mIsCompress) { + if (!next_stage) { + return u_send(pxInd >= compress_data.length); + } else { + return u_send(pxInd >= compress_data2.length); + } + } else { + return u_send(pxInd >= array.length); + } + } + + // Pixel format converting + //----------------------------------------------------- + private boolean u_data(int c, int k1, int k2) + { + if (!load_stage) { + buffArr[0] = (byte)'L'; + buffInd = 1; + load_stage = true; +// System.out.println("u_data: LOAD"); + return u_send(false); + } + + buffInd = 0; + + if(c == -1) + { + if (mIsCompress) { + for (; (pxInd < compress_data.length) && (buffInd < BUFF_SIZE); pxInd++) { +// buffArr[buffInd++] = compress_data[pxInd++]; + buffArr[buffInd++] = compress_data[pxInd]; + } + } else { + while ((pxInd < array.length) && (buffInd + 1 < BUFF_SIZE)) { + int v = 0; + + for (int i = 0; i < 16; i += 2) { + if (pxInd < array.length) v |= (array[pxInd] << i); + pxInd++; + } + + buffArr[buffInd++] = (byte) (v); + buffArr[buffInd++] = (byte) (v >> 8); + } + } + } + else if(c == -2) + { + if (mIsCompress) { + for (; (pxInd < compress_data.length) && (buffInd < BUFF_SIZE); pxInd++) { +// buffArr[buffInd++] = compress_data[pxInd++]; + buffArr[buffInd++] = compress_data[pxInd]; + } + } else { + // 遍历pixel数据,按照缓冲区大小来进行批量填充 + while ((pxInd < array.length) && (buffInd + 1 < BUFF_SIZE)) + { + int v = 0; + + // 从发送数据的角度看,每个字节高4bit和低4bit可以分别存储一个像素颜色值(0~7) + // 所以4个pixel占用2个存储单元 + for(int i = 0; i < 16; i += 4) + { + if (pxInd < array.length) v |= (array[pxInd] << i); + pxInd++; + } + + buffArr[buffInd++] = (byte)(v ); + buffArr[buffInd++] = (byte)(v >> 8); + } + } + + } + else + { + if (mIsCompress) { + if (!next_stage) { + for (; (pxInd < compress_data.length) && (buffInd < BUFF_SIZE); pxInd++) { + buffArr[buffInd++] = compress_data[pxInd]; + } + } else { + // c值为3,代表读取红色像素数据 + for (; (pxInd < compress_data2.length) && (buffInd < BUFF_SIZE); pxInd++) + { + buffArr[buffInd++] = compress_data2[pxInd]; + } + } + } else { + while ((pxInd < array.length) && (buffInd < BUFF_SIZE)) + { + int v = 0; + + for (int i = 0; i < 8; i++) + { + if ((pxInd < array.length) && (array[pxInd] != c)) v |= (128 >> i); + pxInd++; + } + + buffArr[buffInd++] = (byte)v; + } + } + } + + return u_load(k1, k2); + } + + // Pixel format converting (2.13 e-Paper display) + //----------------------------------------------------- + private boolean u_line(int c, int k1, int k2) + { + if (!load_stage) { + buffArr[0] = (byte)'L'; + buffInd = 1; + load_stage = true; + System.out.println("u_line: LOAD"); + return u_send(false); + } + + buffInd = 0; + // 分辨率 122 * 250 + // 遍历像素数组 + // 每行122个像素,需要用122/8=15.25≈16个字节; 246-6=240个字节; 可以存储240/16=15行数据 + // 换句话讲,老程序默认缓冲区大小为256个字节,抛去头部的6个字节,就剩下250个字节,也只够存储15行数据 + // 当把缓冲区扩大为512字节之后,最多可以存储31行,即有效数据字节 6 + 16 * 31 = 502 + while ((pxInd < array.length) && (buffInd < BUFF_SIZE)) // 15*16+6 ,16*8 = 128 + { + int v = 0; + + // 逐行扫描,将每8个像素的0或1,压缩成一个0~255的字节,存储在bufferArr[]里 + for (int i = 0; (i < 8) && (xLine < 122); i++, xLine++){ + // [0,1,0,1,0,1...] + // i=0; array[0]=0; v=0 + // i=1; array[1]=1; v=0 | 0x80>>1 = 0x40 + // i=2; array[2]=0; v=0x40 + // i=3; array[3]=1; v=0x40 | 0x80>>3 = 0x40 | 0x10 = 0x50 + // i=4; array[4]=0; v=0x50 + // i=5; array[5]=1; v=0x50 | 0x80>>5 = 0x50 | 0x4 = 0x54 + // i=6; array[6]=0; v=0x54 + // i=7; array[7]=1; v=0x54 | 0x80>>7 = 0x54 | 0x1 = 0x55 + if (array[pxInd++] != c) v |= (128 >> i); + } + if(xLine >= 122 )xLine = 0; + buffArr[buffInd++] = (byte)v; + } + + return u_load(k1, k2); + } +} diff --git a/app/src/main/java/com/example/epaperuploader/SocketHandlerWifi.java b/app/src/main/java/com/example/epaperuploader/SocketHandlerWifi.java new file mode 100644 index 0000000..a60ccbe --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/SocketHandlerWifi.java @@ -0,0 +1,653 @@ +package com.example.epaperuploader; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.os.Handler; +import android.util.Log; + +import com.example.epaperuploader.image_processing.EPaperDisplay; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Objects; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +class SocketHandlerWifi extends Handler +{ + private static final int BUFF_SIZE_WIFI = 10000; + private static int buffInd; + private static int xLine; + private static String rqMsg; + + private int pxInd; // Pixel index in picture + private int stInd; // Stage index of uploading + private int dSize; // Size of uploaded data by LOAD command + private int[] array; // Values of picture pixels + + private int sendSize; + private boolean next_stage; // 是否分两批发送: 双色或七色fasle,三色true + private boolean load_stage = false; + private boolean mIsCompress = false; + private byte[] compress_data; + private byte[] compress_data2; + + private String mEsp32IpAddress = null; + private String mEsp32HostName = null; + + private final static String TAG = SocketHandlerWifi.class.getSimpleName(); + + public SocketHandlerWifi(String ip_address, String host_hame) + { + super(); + mEsp32IpAddress = ip_address; + mEsp32HostName = host_hame; + } + + // 图像数据压缩 + byte[] compress_image_data(byte[] image_array) { + // 输出字节流 + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + // GZip输出流 + GZIPOutputStream gzipOutputStream; + try { + // 将图像数据导入GZip + gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream); + gzipOutputStream.write(image_array); + // 关闭GZip输出流 + gzipOutputStream.close(); + // 记录压缩后的字节流 + byte[] encode_array = byteArrayOutputStream.toByteArray(); + // 释放输出字节流 + byteArrayOutputStream.flush(); + byteArrayOutputStream.close(); + System.out.println("gzip compress len=" + encode_array.length); + return encode_array; + } catch (IOException e) { + System.out.println("gzip compress error."+e); + return null; + } + } + + // 解压缩图像数据 + byte[] decompress_image_data(byte[] encode_array) { + try { + // 定义临时缓冲区 + byte[] buffer = new byte[1024]; + byte[] image_data; + + // 输出字节流 + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + // 输入字节流 + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(encode_array); + // GZip输入流 + GZIPInputStream gzipInputStream = new GZIPInputStream(byteArrayInputStream); + // 批量写入输出字节流 + int n = 0; + while ((n = gzipInputStream.read(buffer, 0, buffer.length)) > 0) { + outputStream.write(buffer, 0, n); + } + + // 打印解压后的数据 + System.out.println("gzip decompress len=" + outputStream.toByteArray().length); + + image_data = outputStream.toByteArray(); + // 释放流资源 + outputStream.close(); + byteArrayInputStream.close(); + gzipInputStream.close(); + return image_data; + } catch (IOException e) { + System.out.println("gzip decompress error."+e); + return null; + } + } + + private byte[] get_compress_data(int epdInd, int c) { + byte[] byteArr; + int i; + int index = 0; + int pixelId = 0; + + if(epdInd == 25 || epdInd ==37 || epdInd == 43 || epdInd == 47) { + byteArr = new byte[array.length/2]; + while (pixelId < array.length) { + + int v = 0; + // 从发送数据的角度看,每个字节高4bit和低4bit可以分别存储一个像素颜色值(0~7) + // 所以4个pixel占用2个存储单元 + for(i = 0; i < 16; i += 4) + { + if (pixelId < array.length) v |= (array[pixelId] << i); + pixelId++; + } + + byteArr[index++] = (byte)(v ); + byteArr[index++] = (byte)(v >> 8); + } + + } else { + byteArr = new byte[array.length/8]; + while (pixelId < array.length) + { + int v = 0; + + for (i = 0; i < 8; i++) + { + if ((pixelId < array.length) && (array[pixelId] != c)) v |= (128 >> i); + pixelId++; + } + + byteArr[index++] = (byte)v; + } + } + + return byteArr; + } + + // Converts picture pixels into selected pixel format + // and sends EPDx command + //----------------------------------------------------- + public boolean init_image(Bitmap bmp, boolean is_compress) + { + int w = bmp.getWidth(); // Picture with + int h = bmp.getHeight();// Picture height + int epdInd = EPaperDisplay.epdInd; + array = new int[w*h]; // Array of pixels + int i = 0; // Index of pixel in the array of pixels + + Log.e(TAG, "init_image start"); + + // Loading pixels into array + //------------------------------------------------- + // 从处理过的图像里提取像素信息 + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++, i++) + if(epdInd == 25 || epdInd ==37 || epdInd == 43) { + // 7-color: value 0~7 + array[i] = getVal_7color(bmp.getPixel(x, y)); + } + else if (epdInd == 47) { + // New 7-color: value 0~7 + array[i] = getVal_new7color(bmp.getPixel(x, y)); + } else { + // B/W/R: value 0~3 + array[i] = getVal(bmp.getPixel(x, y)); + } + + byte[] byteArr; +// byte[] decompressed_data; + + System.out.println("image array size: " + array.length); + + mIsCompress = is_compress; + // wifi模式不压缩 + mIsCompress = false; + if (mIsCompress) + { + // re-init + compress_data = null; + compress_data2 = null; + + // 数据压缩 + int c = 0; + // 先根据不同屏幕,获取编码数据 + byteArr = get_compress_data(epdInd, c); + // 再对编码结果进行压缩 + compress_data = compress_image_data(byteArr); +// decompressed_data = decompress_image_data(compress_data); + + // 如果是三色屏的话,还需要再准备第二批代表红色的数据 + if ((epdInd == 3) || (epdInd == 39) || (epdInd==0)||(epdInd==6)||(epdInd==7)||(epdInd==9)||(epdInd==12)|| + (epdInd==16)||(epdInd==19)||(epdInd==22)||(epdInd==26)||(epdInd==27)||(epdInd==28)){ + // do nothing + } else if ((epdInd>15 && epdInd < 22) || (epdInd == 45)) { + // do nothing + } else if (epdInd == 25 || epdInd == 37 || epdInd == 43 || epdInd == 47) { + // do nothing + } else { + c = 3; + byteArr = get_compress_data(epdInd, c); + compress_data2 = compress_image_data(byteArr); +// decompressed_data = decompress_image_data(compress_data2); + } + } + + String send_info = "像素:" + array.length + ",发送字节:0"; + MainActivity.updateSendInfo(send_info); + MainActivity.updateProgress(0); + + sendSize = 0; // nof send size + next_stage = false; // if has next stage + + pxInd = 0; + xLine = 0; //2.13inch + stInd = 0; + dSize = 0; + + Log.e(TAG, "init_image end"); + + return true; + } + + void send_data(int k1, int k2, int total_data_size, int loaded_size, String uri, String post_data) throws IOException { + int progress = (k1 + k2*loaded_size/total_data_size); + String x = "" + progress; + + if (x.length() > 5) x = x.substring(0, 5); + + MainActivity.updateProgress(progress); + + // Size of uploaded data + //------------------------------------------------- + String send_info = "像素:" + array.length + ",发送字节:" + loaded_size/2; + + MainActivity.updateSendInfo(send_info); + + httpWrite(uri, post_data); + } + + // The function is executed after every "Ok!" response + // obtained from esp32, which means a previous command + // is complete and esp32 is ready to get the new one. + //----------------------------------------------------- + public boolean handle_uploading_12in48b(String imgBWStr, String imgRStr) throws IOException { + StringBuilder imgBWStr_m1s1m2s2 = new StringBuilder(); + StringBuilder imgRStr_m1s1m2s2 = new StringBuilder(); + + int i = 0; + int start_index = 0; + int end_index = 0; + // 打包第一个区域 + for (i = 0; i < 492; i++) { + start_index = i * 326; + end_index = start_index + 162; + imgBWStr_m1s1m2s2.append(imgBWStr.substring(start_index, end_index)); + imgRStr_m1s1m2s2.append(imgRStr.substring(start_index, end_index)); + } + // 打包第二个区域 + for (i = 0; i < 492; i++) { + start_index = i * 326 + 162; + end_index = start_index + 164; + imgBWStr_m1s1m2s2.append(imgBWStr.substring(start_index, end_index)); + imgRStr_m1s1m2s2.append(imgRStr.substring(start_index, end_index)); + } + // 打包第三个区域 + for (i = 492; i < 984; i++) { + start_index = i * 326; + end_index = start_index + 162; + imgBWStr_m1s1m2s2.append(imgBWStr.substring(start_index, end_index)); + imgRStr_m1s1m2s2.append(imgRStr.substring(start_index, end_index)); + } + // 打包第四个区域 + for (i = 492; i < 984; i++) { + start_index = i * 326 + 162; + end_index = start_index + 164; + imgBWStr_m1s1m2s2.append(imgBWStr.substring(start_index, end_index)); + imgRStr_m1s1m2s2.append(imgRStr.substring(start_index, end_index)); + } + + Log.e(TAG, "package for 12in48b"); + + int bw_data_size = imgBWStr_m1s1m2s2.length(); + int r_data_size = imgRStr_m1s1m2s2.length(); + + Log.e(TAG, "bw_data_size=" + bw_data_size + ", r_data_size=" + r_data_size); + + int batch_size = 30000; + String uri = ""; + String post_data = ""; + int k1 = 0; + int k2 = 50; + start_index = 0; + // 将第一阶段数据进行分批发送 + while (start_index < bw_data_size) { + uri = "LOADA"; + end_index = Math.min((start_index + batch_size), bw_data_size); + post_data = imgBWStr_m1s1m2s2.substring(start_index, end_index); + Log.e(TAG, "LOADA: " + start_index + "->" + end_index); + send_data(k1, k2, bw_data_size, end_index,uri, post_data); + + start_index += batch_size; + } + + Log.e(TAG, "LoadA finish"); + + k1 = 50; + k2 = 50; + start_index = 0; + // 再将第二阶段数据也分批发送 + while (start_index < r_data_size) { + uri = "LOADB"; + end_index = Math.min((start_index + batch_size), r_data_size); + post_data = imgRStr_m1s1m2s2.substring(start_index, end_index); + Log.e(TAG, "LOADB: " + start_index + "->" + end_index); + send_data(k1, k2, r_data_size, end_index,uri, post_data); + + start_index += batch_size; + } + Log.e(TAG, "LoadB finish"); + + // 最后刷新显示 + uri = "SHOW"; + post_data = ""; + httpWrite(uri, post_data); + + Log.e(TAG, "SHOW"); + + return false; + } + + // The function is executed after every "Ok!" response + // obtained from esp32, which means a previous command + // is complete and esp32 is ready to get the new one. + //----------------------------------------------------- + public boolean handle_uploading_9in69b(String imgBWStr, String imgRStr) throws IOException { + StringBuilder imgBWStr_m1s1 = new StringBuilder(); + StringBuilder imgRStr_m1s1 = new StringBuilder(); + + int i = 0; + int start_index = 0; + int end_index = 0; + + int epd_width = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].width; + int epd_height = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].height; + int bytes_per_line = epd_width * 2 / 8; + // 打包第一个区域 + for (i = 0; i < epd_height; i++) { + start_index = i * bytes_per_line; + end_index = start_index + bytes_per_line / 2; + imgBWStr_m1s1.append(imgBWStr.substring(start_index, end_index)); + imgRStr_m1s1.append(imgRStr.substring(start_index, end_index)); + } + // 打包第二个区域 + for (i = 0; i < epd_height; i++) { + start_index = i * bytes_per_line + bytes_per_line / 2; + end_index = start_index + bytes_per_line / 2; + imgBWStr_m1s1.append(imgBWStr.substring(start_index, end_index)); + imgRStr_m1s1.append(imgRStr.substring(start_index, end_index)); + } + + Log.e(TAG, "package for 9in69b"); + + int bw_data_size = imgBWStr_m1s1.length(); + int r_data_size = imgRStr_m1s1.length(); + + Log.e(TAG, "bw_data_size=" + bw_data_size + ", r_data_size=" + r_data_size); + + int batch_size = 30000; + String uri = ""; + String post_data = ""; + int k1 = 0; + int k2 = 50; + start_index = 0; + // 将第一阶段数据进行分批发送 + while (start_index < bw_data_size) { + uri = "LOADA"; + end_index = Math.min((start_index + batch_size), bw_data_size); + post_data = imgBWStr_m1s1.substring(start_index, end_index); + Log.e(TAG, "LOADA: " + start_index + "->" + end_index); + send_data(k1, k2, bw_data_size, end_index,uri, post_data); + + start_index += batch_size; + } + + Log.e(TAG, "LoadA finish"); + + k1 = 50; + k2 = 50; + start_index = 0; + // 再将第二阶段数据也分批发送 + while (start_index < r_data_size) { + uri = "LOADB"; + end_index = Math.min((start_index + batch_size), r_data_size); + post_data = imgRStr_m1s1.substring(start_index, end_index); + Log.e(TAG, "LOADB: " + start_index + "->" + end_index); + send_data(k1, k2, r_data_size, end_index,uri, post_data); + + start_index += batch_size; + } + Log.e(TAG, "LoadB finish"); + + // 最后刷新显示 + uri = "SHOW"; + post_data = ""; + httpWrite(uri, post_data); + + Log.e(TAG, "SHOW"); + + return false; + } + + public boolean handleUploadingStage() throws IOException { + StringBuilder imgBWStr = new StringBuilder(); + StringBuilder imgRStr = new StringBuilder(); + + Log.e(TAG, "handleUploadingStage start"); + + boolean redIsEnabled = (EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].index & 1) != 0; + if (EPaperDisplay.epdInd == 45) { + redIsEnabled = false; + } else if (EPaperDisplay.getDisplays()[EPaperDisplay.epdInd].index > 5) { + redIsEnabled = false; + } + // 根据像素信息,转换成待发送的字符串 + while ((pxInd < array.length)) + { + int v_bw = 0; + int v_r = 0; + int c_bw = 0; + int c_r = 3; + + int epdInd = EPaperDisplay.epdInd; + + // 根据像素值占用bit划分 + if ((epdInd>15 && epdInd < 22) || (epdInd == 45)) { + // 像素值0~3,占用2个bit + for (int i = 0; i < 8; i += 2) { + if (pxInd < array.length) v_bw |= (array[pxInd] << (6-i)); + pxInd++; + } + imgBWStr.append(byteToStr(v_bw)); + } else if (epdInd == 25 || epdInd == 37 || epdInd == 43 || epdInd == 47) { + // 像素值0~6,占用3个bit,实际占用4bit + for (int i = 0; i < 8; i += 4) { + if (pxInd < array.length) v_bw |= (array[pxInd] << (4-i)); + pxInd++; + } + imgBWStr.append(byteToStr(v_bw)); + } else { + // 像素值0~1,占用1个bit + // 这里的编码采用顺序方式,即bit_[x]代表像素[x] + // 先用8->1的方式,将连续的8个像素压缩到一个数据字节里 + for (int i = 0; i < 8; i++) + { + // 0x80 >> i: 0~7-> 0x80, 0x40, 0x20, 0x10, 0x8, 0x4, 0x2, 0 + if ((pxInd < array.length) && (array[pxInd] != c_bw)) v_bw |= (128 >> i); + if (redIsEnabled) { + if ((pxInd < array.length) && (array[pxInd] != c_r)) v_r |= (128 >> i); + } + + pxInd++; + } + + // 然后再将字节转换成字符串,压缩比为1->2 + // 例如0xfe -> 'f' + 'e' + imgBWStr.append(byteToStr(v_bw)); + if (redIsEnabled) { + imgRStr.append(byteToStr(v_r)); + } + } + } + + Log.e(TAG, "-> imgBWStr:" + imgBWStr.length() + ", imgRStr:" + imgRStr.length()); + + Log.e(TAG, String.format("%s", imgBWStr.toString().substring(0, 10))); + + // 对12.48屏进行特殊处理 + if (EPaperDisplay.epdInd == 44) { + return handle_uploading_12in48b(imgBWStr.toString(), imgRStr.toString()); + } else if (EPaperDisplay.epdInd == 46) { + return handle_uploading_9in69b(imgBWStr.toString(), imgRStr.toString()); + } + + Log.e(TAG, "array size=" + array.length); + Log.e(TAG, "imgStr len=" + imgBWStr.length()); + + int start_index = 0; + int end_index = 0; + int bw_data_size = imgBWStr.length(); + int r_data_size = imgRStr.length(); + + String uri = ""; + String post_data = ""; + + int k1, k2; + if (redIsEnabled) { + k1 = 0; + k2 = 50; + } else { + k1 = 0; + k2 = 100; + } + + // 一次性发送的话,如果数据量太大,反而效率有所降低 + int batch_size = 30000; + + start_index = 0; + while (start_index < bw_data_size) { + uri = "LOADA"; + end_index = Math.min((start_index + batch_size), bw_data_size); + post_data = imgBWStr.substring(start_index, end_index); + send_data(k1, k2, bw_data_size, end_index,uri, post_data); + + start_index += batch_size; + } + + // 如果还有Red数据 + if (redIsEnabled) { + k1 = 50; + k2 = 50; + + // 再批量发送 + uri = "LOADB"; + start_index = 0; + while (start_index < r_data_size) { + end_index = Math.min((start_index + batch_size), r_data_size); + post_data = imgRStr.substring(start_index, end_index); + send_data(k1, k2, r_data_size, end_index, uri, post_data); + start_index += batch_size; + } + } + + // 最后刷新显示 + uri = "SHOW"; + post_data = ""; + httpWrite(uri, post_data); + + Log.e(TAG, "handleUploadingStage end"); + + return false; + } + + // Returns the index of color in palette + //----------------------------------------------------- + public int getVal(int color) + { + int r = Color.red(color); + int g = Color.green(color); + int b = Color.blue(color); + +// 黑色:对应00b +// 白色:对应01b +// 黄色:对应10b +// 红色:对应11b +// @ColorInt public static final int BLACK = 0xFF000000; +// @ColorInt public static final int WHITE = 0xFFFFFFFF; +// @ColorInt public static final int RED = 0xFFFF0000; +// @ColorInt public static final int YELLOW = 0xFFFFFF00; + if((r == 0xFF) && (b == 0xFF)) return 1; + if((r == 0x7F) && (b == 0x7F)) return 2; + if((r == 0xFF) && (g == 0xFF)) return 2; + if((r == 0xFF) && (b == 0x00)) return 3; + + return 0; + } + + // Returns the index of color in palette just for 5.65f e-Paper + //----------------------------------------------------- + public int getVal_7color(int color) + { + int r = Color.red(color); + int g = Color.green(color); + int b = Color.blue(color); + + if((r == 0x00) && (g == 0x00) && (b == 0x00)) return 0; + if((r == 0xFF) && (g == 0xFF) && (b == 0xFF)) return 1; + if((r == 0x00) && (g == 0xFF) && (b == 0x00)) return 2; + if((r == 0x00) && (g == 0x00) && (b == 0xFF)) return 3; + if((r == 0xFF) && (g == 0x00) && (b == 0x00)) return 4; + if((r == 0xFF) && (g == 0xFF) && (b == 0x00)) return 5; + if((r == 0xFF) && (g == 0x80) && (b == 0x00)) return 6; + + return 7; + } + + public int getVal_new7color(int color) + { + int r = Color.red(color); + int g = Color.green(color); + int b = Color.blue(color); + + //#define EPD_7IN3E_BLACK 0x0 /// 000 0xFF000000 + //#define EPD_7IN3E_WHITE 0x1 /// 001 0xFFFFFFFF + //#define EPD_7IN3E_YELLOW 0x2 /// 010 0xFFFFFF00 + //#define EPD_7IN3E_RED 0x3 /// 011 0xFFFF0000 + //#define EPD_7IN3E_ORANGE 0x4 /// 100 + //#define EPD_7IN3E_BLUE 0x5 /// 101 0xFF0000FF + //#define EPD_7IN3E_GREEN 0x6 /// 110 0xFF00FF00 + if((r == 0x00) && (g == 0x00) && (b == 0x00)) return 0; + if((r == 0xFF) && (g == 0xFF) && (b == 0xFF)) return 1; + if((r == 0xFF) && (g == 0xFF) && (b == 0x00)) return 2; + if((r == 0xFF) && (g == 0x00) && (b == 0x00)) return 3; + if((r == 0xFF) && (g == 0x80) && (b == 0x00)) return 4; + if((r == 0x00) && (g == 0x00) && (b == 0xFF)) return 5; + if((r == 0x00) && (g == 0xFF) && (b == 0x00)) return 6; + + return 7; + } + + // 高4位 + 低4位 + private String byteToStr(int v) { + return String.valueOf((char)(((v >> 4) & 0xF) + 97)) + String.valueOf((char)((v & 0xF) + 97)); + } + + public void httpWrite(String uri, String post_data) throws IOException { + Log.e(TAG, "ip_address: " + mEsp32IpAddress + ", uri: " + uri); + + URL url = new URL("http://" + mEsp32IpAddress + "/" + uri); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setDoOutput(true); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json"); + + OutputStream os = conn.getOutputStream(); + + byte[] input = post_data.getBytes("utf-8"); + os.write(input, 0, input.length); + + try { + InputStream is = conn.getInputStream(); + } catch (IOException e) { + Log.e(TAG, "get input instream failed."); + } + + os.close(); + conn.disconnect(); + } +} diff --git a/app/src/main/java/com/example/epaperuploader/SpiListAdapter.java b/app/src/main/java/com/example/epaperuploader/SpiListAdapter.java new file mode 100644 index 0000000..ef00340 --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/SpiListAdapter.java @@ -0,0 +1,87 @@ +package com.example.epaperuploader; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.RadioButton; +import android.widget.TextView; + +import java.util.ArrayList; + +//--------------------------------------------------------- +// File list adapter +//--------------------------------------------------------- +public class SpiListAdapter extends BaseAdapter +{ + private ArrayList mConfigInfoList; + private LayoutInflater mInflator; + + public SpiListAdapter(Context context) + { + super(); + mConfigInfoList = new ArrayList<>(); + mInflator = LayoutInflater.from(context); + } + + public void add(SpiPinConfigInfo configInfo) { + mConfigInfoList.add(configInfo); + } + + public void del(int i) { + mConfigInfoList.remove(i); + } + + public void clear() { + mConfigInfoList.clear(); + } + + @Override + public int getCount() { + return mConfigInfoList.size(); + } + + @Override + public SpiPinConfigInfo getItem(int i) { + return mConfigInfoList.get(i); + } + + @Override + public long getItemId(int i) { + return i; + } + + public ArrayList getList() { + return mConfigInfoList; + } + + @Override + public View getView(int i, View view, ViewGroup viewGroup) { + ViewHolder viewHolder; + // General ListView optimization code. + if (view == null) { + view = mInflator.inflate(R.layout.spi_list_item, viewGroup, false); + viewHolder = new ViewHolder(); + viewHolder.config_name = view.findViewById(R.id.config_name); + viewHolder.config_verbose = view.findViewById(R.id.config_verbose); + viewHolder.config_select = view.findViewById(R.id.config_select); + view.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) view.getTag(); + } + + SpiPinConfigInfo configInfo = mConfigInfoList.get(i); + viewHolder.config_name.setText(configInfo.getConfigName()); + viewHolder.config_verbose.setText(configInfo.getConfigVerbose()); + viewHolder.config_select.setChecked(configInfo.getSelectStatus()); + + return view; + } + + private class ViewHolder { + TextView config_name; + TextView config_verbose; + RadioButton config_select; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/epaperuploader/SpiPinConfigActivity.java b/app/src/main/java/com/example/epaperuploader/SpiPinConfigActivity.java new file mode 100644 index 0000000..e2b6614 --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/SpiPinConfigActivity.java @@ -0,0 +1,218 @@ +package com.example.epaperuploader; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.ContextMenu; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ListView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.google.gson.Gson; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +public class SpiPinConfigActivity extends AppCompatActivity { + // View of displays list + //--------------------------------------------------------- + private static ListView mSpiList; + + private static SpiListAdapter mSpiListAdapter = null; + + public static final int SPI_PIN_EDIT = 2; + + static SharedPreferences sharedPreferences = null; + static SharedPreferences.Editor config_editor; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_spi_pin_config); + + getSupportActionBar().setTitle("管脚配置选择 & 添加"); + + sharedPreferences = getSharedPreferences("config", Context.MODE_PRIVATE); + config_editor = sharedPreferences.edit(); + + mSpiListAdapter = new SpiListAdapter(this); + mSpiList = findViewById(R.id.spi_list); + mSpiList.setAdapter(mSpiListAdapter); + + mSpiList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int i, long l) { + set_config_select(i); + } + }); + + recover_spi_list(); + + registerForContextMenu(mSpiList); + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + getMenuInflater().inflate(R.menu.config_menu_sub, menu); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + AdapterView.AdapterContextMenuInfo menuInfo; + menuInfo = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo(); + if (item.getItemId() == R.id.config_menu_delete) { + int item_id = menuInfo.position; + if (item_id == 0) { + Toast.makeText(this, "失败:默认配置不可删除", Toast.LENGTH_SHORT).show(); + } else { + del_spi_config(item_id); + } + } + return super.onContextItemSelected(item); + } + + private void recover_spi_list() { + String config_list_str = sharedPreferences.getString("config_list", ""); + // 如果没有配置过SPI + if (config_list_str.isEmpty()) { + // 则下发默认配置 + // sck=13, din=14, cs=15, busy=25, rst=26, dc=27 + SpiPinConfigInfo configInfo = new SpiPinConfigInfo("默认配置", + 14, 13, 15, 27, 26, 25, false); + add_spi_config(configInfo); + update_config_list(true); + } else { + try { + JSONArray jsonArray = new JSONArray(config_list_str); + JSONObject jsonObject; + for (int i = 0; i < jsonArray.length(); i++) { + jsonObject = jsonArray.getJSONObject(i); + String config_name = jsonObject.optString("config_name"); + int din = jsonObject.getInt("din"); + int sck = jsonObject.getInt("sck"); + int cs = jsonObject.getInt("cs"); + int dc = jsonObject.getInt("dc"); + int rst = jsonObject.getInt("rst"); + int busy = jsonObject.getInt("busy"); + boolean is_select = jsonObject.getBoolean("is_select"); + System.out.println(config_name + ":" + "din=" + din + ", sck=" + sck + ", cs=" + + cs + ", dc=" + dc + ", rst=" + rst + ", busy=" + busy + ", is_select=" + is_select); + add_spi_config(new SpiPinConfigInfo(config_name, din, sck, cs, dc, rst, busy, is_select)); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + + public static ArrayList get_spi_config_list() { + return mSpiListAdapter.getList(); + } + + public void add_spi_config(SpiPinConfigInfo configInfo) { + mSpiListAdapter.add(configInfo); + update_config_list(true); + } + + public void del_spi_config(int item_id) { + mSpiListAdapter.del(item_id); + boolean has_select = false; + // 此处做保护性检查 + for (int i = 0; i < mSpiListAdapter.getCount(); i ++) { + if (mSpiListAdapter.getItem(i).getSelectStatus()) { + has_select = true; + break; + } + } + if (has_select != true) { + mSpiListAdapter.getItem(0).setSelectStatus(true); + setResult(RESULT_OK, new Intent()); + } + update_config_list(true); + } + + private void set_config_select(int select_id) { + for (int i = 0; i < mSpiListAdapter.getCount(); i ++) { + if (i == select_id) { + mSpiListAdapter.getItem(i).setSelectStatus(true); + } else { + mSpiListAdapter.getItem(i).setSelectStatus(false); + } + } + + update_config_list(true); + + setResult(RESULT_OK, new Intent()); + finish(); + } + + private void update_config_list(boolean has_change) { + // 更新表格 + mSpiListAdapter.notifyDataSetChanged(); + + // 保存列表 + if (has_change) { + Gson gson = new Gson(); + String config_list = gson.toJson(mSpiListAdapter.getList()); + + config_editor.putString("config_list", config_list); + config_editor.commit(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == SPI_PIN_EDIT) { + if (resultCode == RESULT_OK) { + String new_config = data.getStringExtra("NEW_CONFIG"); + System.out.println("NEW:" + new_config); + try { + JSONObject jsonObject = new JSONObject(new_config); + + String config_name = jsonObject.optString("config_name"); + int din = jsonObject.getInt("din"); + int sck = jsonObject.getInt("sck"); + int cs = jsonObject.getInt("cs"); + int dc = jsonObject.getInt("dc"); + int rst = jsonObject.getInt("rst"); + int busy = jsonObject.getInt("busy"); + boolean is_select = jsonObject.getBoolean("is_select"); + System.out.println(config_name + ":" + "din=" + din + ", sck=" + sck + ", cs=" + + cs + ", dc=" + dc + ", rst=" + rst + ", busy=" + busy + ", is_select=" + is_select); + add_spi_config(new SpiPinConfigInfo(config_name, din, sck, cs, dc, rst, busy, is_select)); + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + } + + public void addSpiPinConfig(View view) { + startActivityForResult( + new Intent(this, SpiPinEditActivity.class), + SPI_PIN_EDIT); + } + + public static SpiPinConfigInfo getSpiConfig() { + SpiPinConfigInfo configInfo; + for (int i = 0; i < mSpiListAdapter.getCount(); i ++) { + configInfo = mSpiListAdapter.getItem(i); + if (configInfo.getSelectStatus()) { + return configInfo; + } + } + + return null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/epaperuploader/SpiPinConfigInfo.java b/app/src/main/java/com/example/epaperuploader/SpiPinConfigInfo.java new file mode 100644 index 0000000..470eb9a --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/SpiPinConfigInfo.java @@ -0,0 +1,37 @@ +package com.example.epaperuploader; + +//sck=13, din=14, cs=15, busy=25, rst=26, dc=27 +public class SpiPinConfigInfo { + public String config_name; + private int din; + private int sck; + private int cs; + private int dc; + private int rst; + private int busy; + private boolean is_select; + + public SpiPinConfigInfo( + String config_name, + int din, int sck, int cs, int dc, int rst, int busy, boolean is_select ) { + this.config_name = config_name; + this.din = din; + this.sck = sck; + this.cs = cs; + this.dc = dc; + this.rst = rst; + this.busy = busy; + this.is_select = is_select; + } + + public String getConfigName() { + return config_name; + } + public boolean getSelectStatus() { return is_select; } + public void setSelectStatus(boolean select_status) { is_select = select_status; } + + //sck=13, din=14, cs=15, busy=25, rst=26, dc=27 + public String getConfigVerbose() { + return "din=" + din + ", sck=" + sck + ", cs=" + cs + ", dc=" + dc + ", rst=" + rst + ", busy=" + busy; + } +} diff --git a/app/src/main/java/com/example/epaperuploader/SpiPinEditActivity.java b/app/src/main/java/com/example/epaperuploader/SpiPinEditActivity.java new file mode 100644 index 0000000..eb27ee6 --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/SpiPinEditActivity.java @@ -0,0 +1,87 @@ +package com.example.epaperuploader; + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; +import android.widget.EditText; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.google.gson.Gson; + +import java.util.ArrayList; + +public class SpiPinEditActivity extends AppCompatActivity { + + private EditText mConfigName; + private EditText mCsPin; + private EditText mDcPin; + private EditText mDinPin; + private EditText mSckPin; + private EditText mRstPin; + private EditText mBusyPin; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_spi_pin_edit); + + getSupportActionBar().setTitle("配置编辑"); + + mConfigName = findViewById(R.id.edit_config_name); + mCsPin = findViewById(R.id.edit_cs_pin); + mDcPin = findViewById(R.id.edit_dc_pin); + mDinPin = findViewById(R.id.edit_din_pin); + mSckPin = findViewById(R.id.edit_sck_pin); + mRstPin = findViewById(R.id.edit_rst_pin); + mBusyPin = findViewById(R.id.edit_busy_pin); + } + + SpiPinConfigInfo checkEditConfig() { + String config_name = mConfigName.getText().toString().trim(); + String din_pin = mDinPin.getText().toString().trim(); + String sck_pin = mSckPin.getText().toString().trim(); + String cs_pin = mCsPin.getText().toString().trim(); + String dc_pin = mDcPin.getText().toString().trim(); + String rst_pin = mRstPin.getText().toString().trim(); + String busy_pin = mBusyPin.getText().toString().trim(); + if (config_name.isEmpty() + || din_pin.isEmpty() + || sck_pin.isEmpty() + || cs_pin.isEmpty() + || dc_pin.isEmpty() + || rst_pin.isEmpty() + || busy_pin.isEmpty()) { + Toast.makeText(this, "错误:以上填写字段不允许为空", Toast.LENGTH_SHORT).show(); + return null; + } else { + ArrayList spi_config_list = SpiPinConfigActivity.get_spi_config_list(); + for (int i = 0; i < spi_config_list.size(); i++) { + if (config_name.equals(spi_config_list.get(i).getConfigName())) { + Toast.makeText(this, "错误:与已保存的配置名称冲突", Toast.LENGTH_SHORT).show(); + return null; + } + } + SpiPinConfigInfo configInfo = new SpiPinConfigInfo(config_name, + Integer.valueOf(din_pin), Integer.valueOf(sck_pin), Integer.valueOf(cs_pin), + Integer.valueOf(dc_pin), Integer.valueOf(rst_pin), Integer.valueOf(busy_pin), + false + ); + return configInfo; + } + } + + public void saveEditConfig(View view) { + SpiPinConfigInfo configInfo = checkEditConfig(); + if (configInfo == null) { + return; + } + Toast.makeText(this, "配置检查通过", Toast.LENGTH_SHORT).show(); + Intent intent = new Intent(); + Gson gson = new Gson(); + intent.putExtra("NEW_CONFIG", gson.toJson(configInfo)); + setResult(RESULT_OK, intent); + finish(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/epaperuploader/communication/PermissionHelper.java b/app/src/main/java/com/example/epaperuploader/communication/PermissionHelper.java new file mode 100644 index 0000000..efd5119 --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/communication/PermissionHelper.java @@ -0,0 +1,173 @@ +package com.example.epaperuploader.communication; + +import android.Manifest; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.pm.PackageManager; +import android.content.res.Resources; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; + +import com.example.epaperuploader.R; + +/** + *

Permission helper

+ * The class provides functions for delayed operations, + * which require permissions and interaction with user. + * + * @author Waveshare team + * @version 1.0 + * @since 8/16/2018 + */ + +public class PermissionHelper implements DialogInterface.OnDismissListener +{ + // Permissions used in the application + //--------------------------------------------------------- + public static final int REQ_BLUE = 0; // For bluetooth devices scanning + public static final int REQ_READ = 1; // For reading from external storage + + // Permissions related messages + //--------------------------------------------------------- + private static final String[] messages = new String[] + { + "Please accept the GPS Location permission", + "Please accept the Storage reading permission", + "The GPS Location permission is denied", + "The Storage Reading permission is denied", + }; + + // Full names of permissions + //--------------------------------------------------------- + private static final String[] permissions = new String[] + { + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.READ_EXTERNAL_STORAGE, + }; + + // Wrapped names of permissions wrapped (used in requests) + //--------------------------------------------------------- + private static final String[][] permissionArrays = new String[][] + { + new String[] { Manifest.permission.ACCESS_COARSE_LOCATION }, + new String[] { Manifest.permission.READ_EXTERNAL_STORAGE }, + }; + + // Granted permission event handler interface + //--------------------------------------------------------- + public interface PermissionResponse + { + void invoke(); + } + + // Array of granted permission event handlers + //--------------------------------------------------------- + private static PermissionResponse[] responses = new PermissionResponse[] + { + null, + null, + }; + + private Activity context; // Activity which needs a permission + private int reqCode; // + + public PermissionHelper(Activity activity) + { + context = activity; + } + + public AlertDialog show(int requestCode) + { + reqCode = requestCode; + AlertDialog.Builder dialog = new AlertDialog.Builder(context); + + dialog.setTitle(R.string.dlg_note); + dialog.setOnDismissListener(this); + dialog.setPositiveButton(R.string.btn_okey, null); + dialog.setMessage(messages[reqCode]); + + return dialog.show(); + } + + public void onDismiss(DialogInterface dialog) + { + if (reqCode >= permissions.length) return; + ActivityCompat.requestPermissions(context, permissionArrays[reqCode], reqCode); + } + + // Interaction with user and sending the request permission + //--------------------------------------------------------- + public boolean sendRequestPermission(int requestCode) + { + // Get permission if possible + //----------------------------------------------------- + if (requestCode >= permissions.length) return false; + String permission = permissions[requestCode]; + + // Return true if it is already granted + //----------------------------------------------------- + if (context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED) + return true; + + // Interact with user if it's needed + //----------------------------------------------------- + if (context.shouldShowRequestPermissionRationale(permission)) + show(requestCode); + + // Request the permission + //----------------------------------------------------- + else context.requestPermissions(permissionArrays[requestCode], requestCode); + + return false; + } + + // Getting and handling the request permission result + //--------------------------------------------------------- + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) + { + // Unknown result + //----------------------------------------------------- + if ((requestCode < 0) || (requestCode >= permissions.length)) + return; + + // Notify that permission is not granted yet + //----------------------------------------------------- + if (context.checkSelfPermission(permissions[requestCode]) != PackageManager.PERMISSION_GRANTED) + { + if (responses[requestCode] != null) + show(requestCode + permissions.length); + } + + // Perform granted permission's response function + //----------------------------------------------------- + else if (responses[requestCode] != null) + responses[requestCode].invoke(); + + // Clear corresponded cell in responses array + //----------------------------------------------------- + responses[requestCode] = null; + } + + // Set response on + //--------------------------------------------------------- + public void setResponse(int requestCode, PermissionResponse response) + { + if ((requestCode < 0) || (requestCode >= responses.length)) return; + responses[requestCode] = response; + } + + // Notification dialog + //--------------------------------------------------------- + public static void note(Activity activity, int msg) + { + Resources resources = activity.getResources(); + AlertDialog.Builder dialog = new AlertDialog.Builder(activity); + + dialog.setTitle(resources.getString(R.string.dlg_note)); + dialog.setPositiveButton(resources.getString(R.string.btn_okey), null); + dialog.setMessage(resources.getString(msg)); + dialog.show(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/epaperuploader/image_processing/EPaperDisplay.java b/app/src/main/java/com/example/epaperuploader/image_processing/EPaperDisplay.java new file mode 100644 index 0000000..576da87 --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/image_processing/EPaperDisplay.java @@ -0,0 +1,104 @@ +package com.example.epaperuploader.image_processing; + +/** + * Created by YiWan on 8/24/2018. + */ + +/** + *

e-Paper display characteristics

+ * The class is intended to store width, height and index of palette. + * + * @author Waveshare team + * @version 1.0 + * @since 8/14/2018 + */ + +public class EPaperDisplay +{ + public int width; + public int height; + public int index; + public String title; + + /** + * Stores display's characteristics. + * @param width of display in pixels + * @param height of display on pixels + * @param index of palette + * @param title of display + */ + public EPaperDisplay(int width, int height, int index, String title) + { + this.width = width; + this.height = height; + this.index = index; + this.title = title; + } + + // Index of selected display + //--------------------------------------------------------- + public static int epdInd = -1; + + // Array of display characteristics + //--------------------------------------------------------- + private static EPaperDisplay[] array = null; + + public static EPaperDisplay[] getDisplays() + { + if (array == null) + { + array = new EPaperDisplay[] + { + new EPaperDisplay(200,200,0, "1.54 inch e-Paper"), // 0 + new EPaperDisplay(200,200,3, "1.54 inch e-Paper (B)"), // 1 + new EPaperDisplay(152,152,5, "1.54 inch e-Paper (C)"), // 2 + new EPaperDisplay(122,250,0, "2.13 inch e-Paper"), // 3 + new EPaperDisplay(104,212,1, "2.13 inch e-Paper (B)"), // 4 + new EPaperDisplay(104,212,5, "2.13 inch e-Paper (C)"), // 5 + new EPaperDisplay(104,212,0, "2.13 inch e-Paper (D)"), // 6 + new EPaperDisplay(176,264,0, "2.7 inch e-Paper"), // 7 + new EPaperDisplay(176,264,1, "2.7 inch e-Paper (B)"), // 8 + new EPaperDisplay(128,296,0, "2.9 inch e-Paper"), // 9 + new EPaperDisplay(128,296,1, "2.9 inch e-Paper (B)"), // 10 + new EPaperDisplay(128,296,5, "2.9 inch e-Paper (C)"), // 11 + new EPaperDisplay(128,296,0, "2.9 inch e-Paper (D)"), // 12 + new EPaperDisplay(400,300,0, "4.2 inch e-Paper"), // 13 + new EPaperDisplay(400,300,1, "4.2 inch e-Paper (B)"), // 14 + new EPaperDisplay(400,300,5, "4.2 inch e-Paper (C)"), // 15 + new EPaperDisplay(600,448,0, "5.83 inch e-Paper"), // 16 + new EPaperDisplay(600,448,1, "5.83 inch e-Paper (B)"), // 17 + new EPaperDisplay(600,448,5, "5.83 inch e-Paper (C)"), // 18 + new EPaperDisplay(640,384,0, "7.5 inch e-Paper"), // 19 + new EPaperDisplay(640,384,1, "7.5 inch e-Paper (B)"), // 20 + new EPaperDisplay(640,384,5, "7.5 inch e-Paper (C)"), // 21 + new EPaperDisplay(800,480,0, "7.5 inch e-Paper V2"), // 22 + new EPaperDisplay(800,480,1, "7.5 inch e-Paper (B) V2"), // 23 + new EPaperDisplay(880,528,1, "7.5 inch HD e-Paper (B)"), // 24 + new EPaperDisplay(600,448,7, "5.65 inch e-Paper (F)"), // 25 + new EPaperDisplay(880,528,0, "7.5 inch HD e-Paper"), // 26 + new EPaperDisplay(280,480,0, "3.7 inch e-Paper"), // 27 + new EPaperDisplay(152,296,0, "2.66 inch e-Paper"), // 28 + new EPaperDisplay(648,480,1, "5.83 inch e-Paper (B) V2"), // 29 + new EPaperDisplay(128,296,1, "2.9 inch e-Paper (B) V3"), // 30 + new EPaperDisplay(200,200,1, "1.54 inch e-Paper (B) V2"), // 31 + new EPaperDisplay(104,214,1, "2.13 inch e-Paper (B) V3"), // 32 + new EPaperDisplay(128,296,0, "2.9 inch e-Paper V2"), // 33 + new EPaperDisplay(400,300,1, "4.2 inch e-Paper (B) V2"), // 34 + new EPaperDisplay(152,296,1, "2.66 inch e-Paper (B)"), // 35 + new EPaperDisplay(648,480,0, "5.83 inch e-Paper V2"), // 36 + new EPaperDisplay(640,400,7, "4.01 inch e-Paper (F)"), // 37 + new EPaperDisplay(176,264,1, "2.7 inch e-Paper (B) V2"), // 38 + new EPaperDisplay(122,250,0, "2.13 inch e-Paper V3"), // 39 + new EPaperDisplay(122,250,1, "2.13 inch e-Paper (B) V4"), // 40 + new EPaperDisplay(240,360,0, "3.52 inch e-Paper"), // 41 + new EPaperDisplay(176,264,0, "2.7 inch e-Paper V2"), // 42 + new EPaperDisplay(800,480,7, "7.3 inch e-Paper (F)"), // 43 + new EPaperDisplay(1304,984,1, "12.48 inch e-Paper (B) V1"), // 44 + new EPaperDisplay(960,672,8, "9.7 inch e-Paper (G)"), // 45 + new EPaperDisplay(960,672,1, "9.69 inch e-Paper (B)"), // 46 + new EPaperDisplay(800,480,9, "7.3 inch e-Paper (E)"), // 47 + }; // index & 0x1 != 0 代表具有彩色 + } + return array; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/epaperuploader/image_processing/EPaperPicture.java b/app/src/main/java/com/example/epaperuploader/image_processing/EPaperPicture.java new file mode 100644 index 0000000..d17920e --- /dev/null +++ b/app/src/main/java/com/example/epaperuploader/image_processing/EPaperPicture.java @@ -0,0 +1,232 @@ +package com.example.epaperuploader.image_processing; + +import android.graphics.Bitmap; +import android.graphics.Color; + +import com.example.epaperuploader.MainActivity; + +/** + *

Image filtering for e-Paper display

+ * The class is intended for pixel data converting + * from ARGB format to e-Paper display suitable format. + * The result of the pixel format's converting is an index + * of color in e-Paper dislay's palette + * + * @author Waveshare team + * @version 1.0 + * @since 8/14/2018 + */ + +public class EPaperPicture +{ + private static int srcW, srcH; // Width and height of source image + private static int dstW, dstH; // Width and height of destination image + + private static Bitmap srcBmp; // Bitmap of source image + private static Bitmap dstBmp; // Bitmap of destination image + + private static int[] curPal; // Current palette + + private static boolean isImageEnhance; + + private static int[][] palettes = new int[][] // Palettes + { + new int[] { Color.BLACK, Color.WHITE }, + new int[] { Color.BLACK, Color.WHITE, Color.RED }, + new int[] { Color.BLACK, Color.WHITE, Color.GRAY }, + new int[] { Color.BLACK, Color.WHITE, Color.GRAY, Color.RED }, + new int[] { Color.BLACK, Color.WHITE }, + new int[] { Color.BLACK, Color.WHITE, Color.YELLOW }, + new int[] {}, + new int[] { Color.BLACK, Color.WHITE, Color.GREEN, Color.BLUE, Color.RED, Color.YELLOW, 0xffff8000 /* orange */}, + new int[] { Color.BLACK, Color.WHITE, Color.YELLOW, Color.RED }, + new int[] { Color.BLACK, Color.WHITE, Color.YELLOW, Color.RED, 0xffff8000, Color.BLUE, Color.GREEN }, + }; + + // Return the square error of {r, g, b}, + // that means how far them are from standard color stdCol + //--------------------------------------------------------- + private static double getErr(double r, double g, double b, int stdCol) + { + r -= Color.red (stdCol); + g -= Color.green(stdCol); + b -= Color.blue (stdCol); + + return r*r + g*g + b*b; + } + + // Return the index of current palette color which is + // nearest to the {r, g, b} + //--------------------------------------------------------- + private static int getNear(double r, double g, double b) + { + int ind = 0; + double err = getErr(r, g, b, curPal[0]); + + for (int i = 1; i < curPal.length; i++) + { + double cur = getErr(r, g, b, curPal[i]); + if (cur < err) { err = cur; ind = i; } + } + + return ind; + } + + // Return the index of current palette color which is + // nearest to the color clr + //--------------------------------------------------------- + private static int getNear(int clr) + { + return getNear(Color.red(clr), Color.green(clr), Color.blue(clr)); + } + + // Adding of color {r, g, b} into e color array with + // weight k. Here every r, g or b channel takes one cell + // in e color array and can have any integer value. + //--------------------------------------------------------- + private static void addVal(double[] e, int i, double r, double g, double b, double k) + { + int factor = isImageEnhance ? 32 : 16; + int index = i * 3; + e[index ] = (r * k) / factor + e[index ]; + e[index + 1] = (g * k) / factor + e[index + 1]; + e[index + 2] = (b * k) / factor + e[index + 2]; + } + + // Returns a color from the current palette + // which is nearest to source bitmap pixel at (x, y), or + // returns default color if (x, y) is out of the bitmap + //--------------------------------------------------------- + private static int nearColor(int x, int y) + { + if ((x >= srcW) || (y >= srcH)) return curPal[(x + y) % 2 == 0 ? 1 : 0]; + return curPal[getNear(srcBmp.getPixel(x, y))]; + } + + // Returns bitmap of pixels from current palette. + // + // isLvl = true - the method of searching the nearest color + // is based on estimation how less the original pixel + // differs from the palette's pixels. + // + // isLvl = false - the method of searching the nearest color + // is based on estimation how less the original pixel's area + // differs from the palette's pixels (so called dithering). + //--------------------------------------------------------- + public static Bitmap createIndexedImage(boolean isLvl, boolean isRed, boolean isEnhance) + { + System.out.println("createIndexedImage is calling, " + isLvl + "," + isRed + ',' + isEnhance); + + EPaperDisplay epd = EPaperDisplay.getDisplays()[EPaperDisplay.epdInd]; + srcBmp = MainActivity.originalImage; + dstBmp = Bitmap.createBitmap(epd.width, epd.height, srcBmp.getConfig()); + isImageEnhance = isEnhance; + + int palInd = epd.index; + if (!isRed) palInd = palInd & 0xE; + + curPal = palettes[palInd]; + + dstW = dstBmp.getWidth(); + dstH = dstBmp.getHeight(); + + srcW = srcBmp.getWidth(); + srcH = srcBmp.getHeight(); + + int[] srcArr = new int[srcW * srcH]; + int[] dstArr = new int[dstW * dstH]; + + int index = 0; + srcBmp.getPixels(srcArr, 0, srcW, 0, 0, srcW, srcH); + + if (isLvl) + { + for (int y = 0; y < dstH; y++) + for (int x = 0; x < dstW; x++) { + dstArr[index++] = nearColor(x, y); + } + } + else + { + int aInd = 0; + int bInd = 1; + + double[][] errArr = new double[2][]; + + errArr[0] = new double[3*dstW]; + errArr[1] = new double[3*dstW]; + + for (int i = 0; i < dstW; i++) + { + errArr[bInd][3*i ] = 0; + errArr[bInd][3*i + 1] = 0; + errArr[bInd][3*i + 2] = 0; + } + + for (int j = 0; j < dstH; j++) + { + if (j >= srcH) + { + for (int i = 0; i < dstW; i++, index++) + dstArr[index] = curPal[(i + j) % 2 == 0 ? 1 : 0]; + continue; + } + + aInd = ((bInd = aInd) + 1) & 1; + + for (int i = 0; i < dstW; i++) + { + errArr[bInd][3*i ] = 0; + errArr[bInd][3*i + 1] = 0; + errArr[bInd][3*i + 2] = 0; + } + + for (int i = 0; i < dstW; i++) + { + if (i >= srcW) + { + dstArr[index++] = curPal[(i + j) % 2 == 0 ? 1 : 0]; + continue; + } + + int srcPix = srcArr[j * srcW + i]; + + double r = Color.red (srcPix) + errArr[aInd][3*i ]; + double g = Color.green(srcPix) + errArr[aInd][3*i + 1]; + double b = Color.blue (srcPix) + errArr[aInd][3*i + 2]; + + int colVal = curPal[getNear(r, g, b)]; + dstArr[index++] = colVal; + + r -= Color.red (colVal); + g -= Color.green(colVal); + b -= Color.blue (colVal); + + if (i == 0) + { + addVal(errArr[bInd], (i ), r, g, b, 7.0); + addVal(errArr[bInd], (i + 1), r, g, b, 2.0); + addVal(errArr[aInd], (i + 1), r, g, b, 7.0); + } + else if (i == dstW - 1) + { + addVal(errArr[bInd], (i - 1), r, g, b, 7.0); + addVal(errArr[bInd], (i ), r, g, b, 9.0); + } + else + { + addVal(errArr[bInd], (i - 1), r, g, b, 3.0); + addVal(errArr[bInd], (i ), r, g, b, 5.0); + addVal(errArr[bInd], (i + 1), r, g, b, 1.0); + addVal(errArr[aInd], (i + 1), r, g, b, 7.0); + } + } + } + } + + // Put converted pixels into destination image bitmap + //----------------------------------------------------- + dstBmp.setPixels(dstArr, 0, dstW, 0, 0, dstW, dstH); + return dstBmp; + } +} diff --git a/app/src/main/res/drawable/bt.xml b/app/src/main/res/drawable/bt.xml new file mode 100644 index 0000000..8087351 --- /dev/null +++ b/app/src/main/res/drawable/bt.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/image.xml b/app/src/main/res/drawable/image.xml new file mode 100644 index 0000000..364bf08 --- /dev/null +++ b/app/src/main/res/drawable/image.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/pin.xml b/app/src/main/res/drawable/pin.xml new file mode 100644 index 0000000..454e43f --- /dev/null +++ b/app/src/main/res/drawable/pin.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/reboot.xml b/app/src/main/res/drawable/reboot.xml new file mode 100644 index 0000000..ffeb988 --- /dev/null +++ b/app/src/main/res/drawable/reboot.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/right_menu.xml b/app/src/main/res/drawable/right_menu.xml new file mode 100644 index 0000000..850600c --- /dev/null +++ b/app/src/main/res/drawable/right_menu.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/wifi.xml b/app/src/main/res/drawable/wifi.xml new file mode 100644 index 0000000..660b9f1 --- /dev/null +++ b/app/src/main/res/drawable/wifi.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/layout/activity_crop_image.xml b/app/src/main/res/layout/activity_crop_image.xml new file mode 100644 index 0000000..83011c7 --- /dev/null +++ b/app/src/main/res/layout/activity_crop_image.xml @@ -0,0 +1,44 @@ + + + + + + + + +