diff --git a/.gitignore b/.gitignore index de83d0a..ea6568a 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ gui/ *.bat *.eep *.lss +tools_python/test_mul8.py diff --git a/PriceHax/PriceHax_21.apk b/PriceHax/PriceHax_21.apk new file mode 100644 index 0000000..d40fe51 Binary files /dev/null and b/PriceHax/PriceHax_21.apk differ diff --git a/PriceHax/app/build.gradle b/PriceHax/app/build.gradle index 55702c6..6eab4ec 100644 --- a/PriceHax/app/build.gradle +++ b/PriceHax/app/build.gradle @@ -4,20 +4,21 @@ plugins { android { namespace 'org.furrtek.pricehax2' - compileSdk 32 + compileSdk 33 defaultConfig { applicationId "org.furrtek.pricehax2" minSdk 24 - targetSdk 32 - versionCode 2 - versionName "2.0" + targetSdk 33 + versionCode 3 + versionName "2.1" ndk.abiFilters 'armeabi-v7a','arm64-v8a','x86_64' } buildTypes { release { minifyEnabled false + shrinkResources false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } diff --git a/PriceHax/app/src/main/ic_launcher-playstore.png b/PriceHax/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..bd0f80b Binary files /dev/null and b/PriceHax/app/src/main/ic_launcher-playstore.png differ diff --git a/PriceHax/app/src/main/java/org/furrtek/pricehax2/CameraPreview.java b/PriceHax/app/src/main/java/org/furrtek/pricehax2/CameraPreview.java index a123bb6..f41aae9 100644 --- a/PriceHax/app/src/main/java/org/furrtek/pricehax2/CameraPreview.java +++ b/PriceHax/app/src/main/java/org/furrtek/pricehax2/CameraPreview.java @@ -30,18 +30,17 @@ public class CameraPreview extends SurfaceView implements Callback { //setCameraDisplayOrientation((Activity) this.context, this.mCamera); } - //public CameraPreview(Context context, Camera camera, PreviewCallback previewCb, AutoFocusCallback autoFocusCb) { public CameraPreview(Context context, PreviewCallback previewCb, AutoFocusCallback autoFocusCb) { super(context); this.context = context; - - this.mCamera = getBackCamera(); this.previewCallback = previewCb; this.autoFocusCallback = autoFocusCb; this.mHolder.addCallback(this); - //this.mHolder.setType(3); + } - //this.cameraId = getBackCameraId(); + public void surfaceCreated(SurfaceHolder holder) { + this.mCamera = getBackCamera(); + //this.mHolder.setType(3); // The old mode uses the onAutoFocus function and makes a timer to try to focus periodically. // The drawback is the hunting of the camera. @@ -51,24 +50,15 @@ public class CameraPreview extends SurfaceView implements Callback { this.oldAutoFocusMode = false; if (params.getSupportedFocusModes().contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); - } - else if (params.getSupportedFocusModes().contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { + } else if (params.getSupportedFocusModes().contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) { params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO); - } - else { + } else { this.oldAutoFocusMode = true; } this.mCamera.setParameters(params); - //this.mCamera.setPreviewCallback(previewCb); this.mCamera.startPreview(); - } - public boolean isOldAutoFocusMode() { - return this.oldAutoFocusMode; - } - - public void surfaceCreated(SurfaceHolder holder) { try { if (this.mCamera != null) { this.mCamera.setPreviewDisplay(holder); @@ -79,6 +69,14 @@ public class CameraPreview extends SurfaceView implements Callback { } public void surfaceDestroyed(SurfaceHolder holder) { + try { + mCamera.stopPreview(); + mCamera.setPreviewCallback(null); + mCamera.release(); + mCamera = null; + } catch (Exception e) { + e.printStackTrace(); + } } public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { @@ -94,9 +92,8 @@ public class CameraPreview extends SurfaceView implements Callback { this.mCamera.setPreviewCallback(this.previewCallback); this.mCamera.startPreview(); - if (this.oldAutoFocusMode) { + if (this.oldAutoFocusMode) this.mCamera.autoFocus(this.autoFocusCallback); - } } catch (Exception e2) { Log.d("DBG", "Error starting camera preview: " + e2.getMessage()); @@ -128,7 +125,6 @@ public class CameraPreview extends SurfaceView implements Callback { switch (rotation) { case Surface.ROTATION_0: degrees = 90; - //params.setPreviewSize(height, width); break; case Surface.ROTATION_90: degrees = 0; break; case Surface.ROTATION_180: degrees = 0; break; @@ -136,18 +132,10 @@ public class CameraPreview extends SurfaceView implements Callback { } Log.d("DBG", "Preview surface rotation: " + degrees); - int result; - result = degrees; - /*if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { - result = (info.orientation + degrees) % 360; - result = (360 - result) % 360; // compensate the mirror - } else { // back-facing - result = (info.orientation - degrees + 360) % 360; - }*/ Camera.Size size = params.getPreviewSize(); Log.d("DBG", "getPreviewSize: " + size.width + "," + size.height); - camera.setDisplayOrientation(result); + camera.setDisplayOrientation(degrees); camera.setParameters(params); } } diff --git a/PriceHax/app/src/main/java/org/furrtek/pricehax2/DMConvert.java b/PriceHax/app/src/main/java/org/furrtek/pricehax2/DMConvert.java index 231e86e..018efc3 100644 --- a/PriceHax/app/src/main/java/org/furrtek/pricehax2/DMConvert.java +++ b/PriceHax/app/src/main/java/org/furrtek/pricehax2/DMConvert.java @@ -1,6 +1,7 @@ package org.furrtek.pricehax2; import android.graphics.Bitmap; +import android.graphics.BitmapShader; import android.os.AsyncTask; import android.util.Log; import android.widget.ProgressBar; @@ -13,7 +14,8 @@ import java.util.List; import static org.furrtek.pricehax2.DitherBitmap.floydSteinbergDithering; -public class DMConvert extends AsyncTask { +public class DMConvert extends AsyncTask { + // This does the color conversion, dithering, encoding and compression. private ImageView mImageView; private ProgressBar mProgressBar; AsyncResponse delegate = null; @@ -28,91 +30,85 @@ public class DMConvert extends AsyncTask { } @Override protected void onPostExecute(DMImage dmimage) { - Log.d("PHX", "Convert done, bytestream size: " + dmimage.byteStreamBW.size()); + Log.d("PHX", "Convert done, BW bytestream size: " + dmimage.byteStreamBW.size()); delegate.processFinish(dmimage); } @Override - protected DMImage doInBackground(Bitmap... selectedImage) { - int x, y, wi, hi; - int w, h; + protected DMImage doInBackground(DMConvertParams... params) { + int x, y, w, h; int pixel; int idx = 0; Bitmap imageBW, imageBWR; - w = selectedImage[0].getWidth(); - h = selectedImage[0].getHeight(); + Bitmap imageIn = params[0].imageIn; + boolean dithering = params[0].dithering; + + w = imageIn.getWidth(); + h = imageIn.getHeight(); DMImage dmimage = new DMImage(w, h); - /*if (PLType == 1318) { - w = 0xD0; - h = 0x70; - } else { - w = 172; - h = 72; - }*/ - wi = w; - hi = h; + BitSet bitstreamBW = new BitSet(w * h); // One plane + BitSet bitstreamBWR = new BitSet(w * h * 2); // Two planes - BitSet bitstreamBW = new BitSet(wi * hi); - BitSet bitstreamBWR = new BitSet(wi * hi * 2); - - Bitmap scaledimage = selectedImage[0]; - - //Convert to BW - Log.d("PHX", "Convert to BW"); - imageBW = floydSteinbergDithering(scaledimage, false); - //dmimage.bitmapBW = imageBW; + // Convert to BW + imageBW = dithering ? floydSteinbergDithering(imageIn, false) : imageIn; for (y = 0; y < h; y++) { for (x = 0; x < w; x++, idx++) { pixel = imageBW.getPixel(x, y); - if (Color.red(pixel) > 0) + if (Color.red(pixel) > 127) { bitstreamBW.set(idx); - else + pixel = Color.WHITE; + } else { bitstreamBW.clear(idx); + pixel = Color.BLACK; + } dmimage.bitmapBW.setPixel(x, y, pixel); } } //Convert to BWR - Log.d("PHX", "Convert to BWR"); - imageBWR = floydSteinbergDithering(scaledimage, true); - //dmimage.bitmapBWR = imageBWR; + imageBWR = dithering ? floydSteinbergDithering(imageIn, true) : imageIn; idx = 0; - int idxr = w * h; + int idxr = w * h; // Offset index for red layer for (y = 0; y < h; y++) { for (x = 0; x < w; x++, idx++, idxr++) { pixel = imageBWR.getPixel(x, y); - if (Color.red(pixel) > 0) { - if (Color.green(pixel) > 0) { + if (Color.red(pixel) > 127) { + if (Color.green(pixel) > 127) { bitstreamBWR.set(idx); // White bitstreamBWR.set(idxr); - dmimage.bitmapBWR.setPixel(x, y, 0xFFFFFFFF); + pixel = Color.WHITE; + //dmimage.bitmapBWR.setPixel(x, y, 0xFFFFFFFF); } else { bitstreamBWR.clear(idx); // Red bitstreamBWR.clear(idxr); - dmimage.bitmapBWR.setPixel(x, y, 0xFFFF0000); + pixel = Color.RED; + //dmimage.bitmapBWR.setPixel(x, y, 0xFFFF0000); } } else { bitstreamBWR.clear(idx); // Black bitstreamBWR.set(idxr); - dmimage.bitmapBWR.setPixel(x, y, 0xFF000000); + pixel = Color.BLACK; + //dmimage.bitmapBWR.setPixel(x, y, 0xFF000000); } + dmimage.bitmapBWR.setPixel(x, y, pixel); } } + // Compress BitSet bitstreamBW_RLE = RLECompress(bitstreamBW); BitSet bitstreamBWR_RLE = RLECompress(bitstreamBWR); - publishProgress(90); + // Pack to bytes dmimage.byteStreamBW = bitstreamBytes(bitstreamBW_RLE); dmimage.byteStreamBWR = bitstreamBytes(bitstreamBWR_RLE); - publishProgress(100); return dmimage; } private List bitstreamBytes(BitSet bitstream) { + // Pack bitstream to bytes List result = new ArrayList(); byte[] bytes = bitstream.toByteArray(); for (byte b : bytes) { @@ -129,9 +125,9 @@ public class DMConvert extends AsyncTask { int idx = result.size(); int klp = idx % 20; if (klp > 0) klp = 20 - klp; - Log.d("PHX", String.format("Padding %d to %d", idx, idx + klp)); for (int bsc = 0; bsc < klp; bsc++) result.add(new Byte((byte)0)); + Log.d("PHX", String.format("Padded %d to %d", idx, idx + klp)); return result; } @@ -144,7 +140,6 @@ public class DMConvert extends AsyncTask { List RLErun = new ArrayList(); // RLE compress - Log.d("PHX", String.format("RLE compress, raw size = %d", bitstream.length())); for (int m = 1; m <= j; m++) { n = bitstream.get(m); if (n == p) { @@ -161,7 +156,7 @@ public class DMConvert extends AsyncTask { BitSet bitstreamRLE = new BitSet(); bitstreamRLE.set(0, bitstream.get(0)); - Log.d("PHX", "Gen unary coded runs"); + // Gen unary coded runs int idx = 1; for (int cnts : RLErun) { bs = Integer.toBinaryString(cnts); @@ -176,11 +171,13 @@ public class DMConvert extends AsyncTask { } } - String dbg = ""; + Log.d("PHX", String.format("RLE compress, %d -> %d", bitstream.length(), bitstreamRLE.length())); + + /*String dbg = ""; for (int i = 0; i < 256; i++) { dbg += (bitstreamRLE.get(i) ? "1" : "0"); } - Log.d("PHX", dbg); + Log.d("PHX", dbg);*/ return bitstreamRLE; } } \ No newline at end of file diff --git a/PriceHax/app/src/main/java/org/furrtek/pricehax2/DMConvertParams.java b/PriceHax/app/src/main/java/org/furrtek/pricehax2/DMConvertParams.java new file mode 100644 index 0000000..f950080 --- /dev/null +++ b/PriceHax/app/src/main/java/org/furrtek/pricehax2/DMConvertParams.java @@ -0,0 +1,13 @@ +package org.furrtek.pricehax2; + +import android.graphics.Bitmap; + +public class DMConvertParams { + Bitmap imageIn; + boolean dithering = false; + + DMConvertParams(Bitmap imageIn, boolean dithering) { + this.imageIn = imageIn; + this.dithering = dithering; + } +} diff --git a/PriceHax/app/src/main/java/org/furrtek/pricehax2/DMGen.java b/PriceHax/app/src/main/java/org/furrtek/pricehax2/DMGen.java index 7c9a90d..241a0a7 100644 --- a/PriceHax/app/src/main/java/org/furrtek/pricehax2/DMGen.java +++ b/PriceHax/app/src/main/java/org/furrtek/pricehax2/DMGen.java @@ -20,13 +20,14 @@ public class DMGen { // Wake-up frame Byte[] payload = {(byte) 0x17, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}; - frames.add(new IRFrame(PLID, (byte)0x85, Arrays.asList(payload), 30, 200)); // TODO: Check delay and repeats + frames.add(new IRFrame(PLID, (byte)0x85, Arrays.asList(payload), 10, 400)); // 400 Same parameters as Python tools // Start frame - int datalen = dmImage.byteStreamBW.size(); // TODO: Pass selected BW or BWR bytestream instead of dmImage + List byteStream = BWR ? dmImage.byteStreamBWR : dmImage.byteStreamBW; + int datalen = byteStream.size(); int width = dmImage.bitmapBW.getWidth(); int height = dmImage.bitmapBW.getHeight(); - Log.d("PHX", String.format("w,h: %d,%d", width, height)); + Log.d("PHX", String.format("Dimensions: %d*%d", width, height)); int x = 0; int y = 0; Byte[] payload_start = { @@ -44,31 +45,30 @@ public class DMGen { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; - frames.add(new IRFrame(PLID, (byte)0x85, Arrays.asList(payload_start), 30, 1)); // TODO: Check delay and repeats + frames.add(new IRFrame(PLID, (byte)0x85, Arrays.asList(payload_start), 10, 1)); // Data frames - Log.d("PHX", String.format("Datalen %d", datalen)); - int ymax = (int) Math.ceil((double)datalen / 20); - Log.d("PHX", String.format("ymax %d", ymax)); - for (y = 0; y < ymax; y++) { + Log.d("PHX", String.format("datalen: %d", datalen)); + int frame_count = (int)Math.ceil((double)datalen / 20); + Log.d("PHX", String.format("frame_count: %d", frame_count)); + for (int frame_idx = 0; frame_idx < frame_count; frame_idx++) { Byte[] payload_data = new Byte[27]; - Log.d("PHX", String.format("Gen data frame %d", y)); payload_data[0] = (byte) 0x34; payload_data[1] = (byte) 0; payload_data[2] = (byte) 0; payload_data[3] = (byte) 0; payload_data[4] = (byte) 0x20; - payload_data[5] = (byte) (y >> 8); - payload_data[6] = (byte) (y & 255); - for (int cp = 0; cp < 20; cp++) { // WAS 20 - payload_data[7 + cp] = dmImage.byteStreamBW.get(cp + (y * 20)); + payload_data[5] = (byte) (frame_idx >> 8); + payload_data[6] = (byte) (frame_idx & 255); + for (int cp = 0; cp < 20; cp++) { + payload_data[7 + cp] = byteStream.get(cp + (frame_idx * 20)); } - frames.add(new IRFrame(PLID, (byte)0x85, Arrays.asList(payload_data), 30, 1)); // TODO: Check delay and repeats + frames.add(new IRFrame(PLID, (byte)0x85, Arrays.asList(payload_data), 10, 1)); } // Refresh frame Byte[] payloadc = {(byte) 0x34, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - frames.add(new IRFrame(PLID, (byte)0x85, Arrays.asList(payloadc), 30, 1)); // TODO: Check delay and repeats + frames.add(new IRFrame(PLID, (byte)0x85, Arrays.asList(payloadc), 10, 1)); return frames; } diff --git a/PriceHax/app/src/main/java/org/furrtek/pricehax2/DitherBitmap.java b/PriceHax/app/src/main/java/org/furrtek/pricehax2/DitherBitmap.java index 15ad9e7..0dec08b 100644 --- a/PriceHax/app/src/main/java/org/furrtek/pricehax2/DitherBitmap.java +++ b/PriceHax/app/src/main/java/org/furrtek/pricehax2/DitherBitmap.java @@ -2,7 +2,6 @@ package org.furrtek.pricehax2; import android.graphics.Bitmap; import android.graphics.Color; -import android.util.Log; public class DitherBitmap { // Atkinson dither might give better results on ESLs diff --git a/PriceHax/app/src/main/java/org/furrtek/pricehax2/ESLBlaster.java b/PriceHax/app/src/main/java/org/furrtek/pricehax2/ESLBlaster.java index a344b4b..72cccba 100644 --- a/PriceHax/app/src/main/java/org/furrtek/pricehax2/ESLBlaster.java +++ b/PriceHax/app/src/main/java/org/furrtek/pricehax2/ESLBlaster.java @@ -1,6 +1,5 @@ package org.furrtek.pricehax2; -import android.graphics.Bitmap; import android.os.AsyncTask; import android.util.Log; import android.widget.ProgressBar; @@ -9,8 +8,6 @@ import android.widget.TextView; import com.hoho.android.usbserial.driver.UsbSerialPort; import java.io.IOException; -import java.util.ArrayList; -import java.util.BitSet; import java.util.List; public class ESLBlaster extends AsyncTask, TXProgress, Boolean> { @@ -18,14 +15,16 @@ public class ESLBlaster extends AsyncTask, TXProgress, Boolean> { private ProgressBar mProgressBar; AsyncResponseTX delegate = null; UsbSerialPort usbSerialPort; + int HWVersion; TextView mTextView; TXProgress progress = new TXProgress(0, ""); - ESLBlaster(UsbSerialPort usbserialport, ProgressBar progressBar, TextView textView, AsyncResponseTX asyncResponse) { - delegate = asyncResponse; - mProgressBar = progressBar; - mTextView = textView; - usbSerialPort = usbserialport; + ESLBlaster(UsbSerialPort usbserialport, int HWVersion, ProgressBar progressBar, TextView textView, AsyncResponseTX asyncResponse) { + this.delegate = asyncResponse; + this.mProgressBar = progressBar; + this.mTextView = textView; + this.usbSerialPort = usbserialport; + this.HWVersion = HWVersion; } @Override protected void onProgressUpdate(TXProgress... progress) { @@ -42,6 +41,8 @@ public class ESLBlaster extends AsyncTask, TXProgress, Boolean> { @Override protected Boolean doInBackground(List... frames) { int i = 1; + boolean PP16 = (HWVersion >= 2); + Log.d("PHX", "PHY mode: " + (PP16 ? "PP16" : "PP4")); int cnt = frames[0].size(); for (IRFrame frame : frames[0]) { /*try { @@ -51,14 +52,19 @@ public class ESLBlaster extends AsyncTask, TXProgress, Boolean> { return false; }*/ - List list = frame.getRawData(false); + List list = frame.getRawData(PP16); byte[] data = new byte[list.size() + 5 + 1]; data[0] = (byte)'L'; data[1] = (byte)list.size(); - data[2] = (byte)frame.delay; // Delay between repeats - data[3] = (byte)(frame.repeats & 255); // Number of repeats - data[4] = (byte)(frame.repeats >> 8); + data[2] = (byte)frame.delay; // Delay between repeats + int repeats = frame.repeats; + if (repeats > 32767) + repeats = 32767; // Cap to 15 bits because FW V2 and up uses MSB to indicate PP16 protocol + if (PP16) + repeats |= 0x8000; + data[3] = (byte)(repeats & 255); // Number of repeats + data[4] = (byte)(repeats >> 8); for (int c = 0; c < list.size(); c++) data[c + 5] = list.get(c).byteValue(); data[data.length - 1] = (byte)'T'; @@ -80,10 +86,10 @@ public class ESLBlaster extends AsyncTask, TXProgress, Boolean> { publishProgress(progress); } - // 10s timeout should be enough + // 20s timeout should be enough // Split the timeout to check if task was cancelled during the wait int w = 0; - for (w = 0; w < 10; w++) { + for (w = 0; w < 20; w++) { if (isCancelled()) { endTX(); return false; @@ -94,7 +100,7 @@ public class ESLBlaster extends AsyncTask, TXProgress, Boolean> { break; }; } - if (w == 10) { + if (w == 20) { Log.d("PHX", "Comm timeout"); endTX(); // Timed out return false; diff --git a/PriceHax/app/src/main/java/org/furrtek/pricehax2/IRFrame.java b/PriceHax/app/src/main/java/org/furrtek/pricehax2/IRFrame.java index 4d76221..650cc10 100644 --- a/PriceHax/app/src/main/java/org/furrtek/pricehax2/IRFrame.java +++ b/PriceHax/app/src/main/java/org/furrtek/pricehax2/IRFrame.java @@ -48,16 +48,14 @@ class IRFrame { if (PP16) { // Special PP16 header Byte[] header = {0x00, 0x00, 0x00, (byte)0x40}; - frame_data.addAll(Arrays.asList(header)); + frame_data.addAll(0, Arrays.asList(header)); } - // Debug - String hex_str = ""; + /*String hex_str = ""; for (byte b : frame_data) { hex_str += String.format("%02X ", b); } - - Log.d("getRawData", hex_str); + Log.d("PHX", "getRawData " + hex_str);*/ return frame_data; } diff --git a/PriceHax/app/src/main/java/org/furrtek/pricehax2/MainActivity.java b/PriceHax/app/src/main/java/org/furrtek/pricehax2/MainActivity.java index 35ee92f..603231d 100644 --- a/PriceHax/app/src/main/java/org/furrtek/pricehax2/MainActivity.java +++ b/PriceHax/app/src/main/java/org/furrtek/pricehax2/MainActivity.java @@ -1,6 +1,7 @@ package org.furrtek.pricehax2; import android.Manifest; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.app.PendingIntent; @@ -15,49 +16,46 @@ import android.hardware.Camera; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; import android.hardware.usb.UsbManager; -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultCallback; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.coordinatorlayout.widget.CoordinatorLayout; -import com.hoho.android.usbserial.driver.UsbSerialDriver; -import com.hoho.android.usbserial.driver.UsbSerialPort; -import com.hoho.android.usbserial.driver.UsbSerialProber; -import com.hoho.android.usbserial.util.SerialInputOutputManager; -import java.io.IOException; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.util.Log; -import android.widget.*; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import com.google.android.material.snackbar.Snackbar; -import androidx.appcompat.app.AppCompatActivity; -import android.view.View; -import androidx.navigation.ui.AppBarConfiguration; - import android.view.Menu; import android.view.MenuItem; +import android.view.View; +import android.widget.*; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.coordinatorlayout.widget.CoordinatorLayout; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +import com.google.android.material.snackbar.Snackbar; +import com.hoho.android.usbserial.driver.UsbSerialDriver; +import com.hoho.android.usbserial.driver.UsbSerialPort; +import com.hoho.android.usbserial.driver.UsbSerialProber; +import com.hoho.android.usbserial.util.SerialInputOutputManager; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; import net.sourceforge.zbar.Config; import net.sourceforge.zbar.Image; import net.sourceforge.zbar.ImageScanner; import net.sourceforge.zbar.Symbol; import org.furrtek.pricehax2.databinding.ActivityMainBinding; - -import java.nio.charset.StandardCharsets; -import java.util.*; - -import static android.provider.Settings.System.getString; -import static androidx.core.app.ActivityCompat.finishAffinity; +import org.jetbrains.annotations.NotNull; public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding; private Handler autoFocusHandler; - private CameraPreview mPreview; CoordinatorLayout mainlayout; private enum UsbPermission { Unknown, Requested, Granted, Denied } private static final String INTENT_ACTION_GRANT_USB = BuildConfig.APPLICATION_ID + ".GRANT_USB"; @@ -72,12 +70,12 @@ public class MainActivity extends AppCompatActivity { int imageScale = 100; DMImage dmImage = null; boolean inBWR = false; + boolean dithering = false; private boolean blasterConnected = false; char blasterHWVersion; int blasterFWVersion; Uri selectedImageUri = null; int dispDurationIdx, dispPage; - //AsyncResponseTX asyncTaskTX = null; boolean transmitting = false; boolean loopTX = false; boolean autoTX = false; @@ -100,36 +98,31 @@ public class MainActivity extends AppCompatActivity { setSupportActionBar(binding.toolbar); - mainlayout = (CoordinatorLayout) findViewById(R.id.coordinatorLayout); + mainlayout = binding.coordinatorLayout; //(CoordinatorLayout) findViewById(R.id.coordinatorLayout); - // App e-stop - binding.main.fab.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - finishAffinity(); - System.exit(0); - } + // App e-stop (disabled for now, not very useful) + binding.main.fab.setOnClickListener(view -> { + finishAffinity(); + System.exit(0); }); // Stop TX - binding.main.buttonTXStop.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (transmitting) { - byte[] data = {(byte)'S'}; - try { - usbSerialPort.write(data, 1000); - } catch(IOException ignore) { - } - binding.main.switchLoopTX.setChecked(false); + binding.main.buttonTXStop.setOnClickListener(view -> { + if (transmitting) { + // Tell blaster to stop TX + byte[] data = {(byte)'S'}; + try { + usbSerialPort.write(data, 1000); + } catch(IOException ignore) { } + binding.main.switchLoopTX.setChecked(false); // Disable loop mode } }); // Populate duration spinner Spinner spinner = binding.main.spinnerDuration; List arrayList = Arrays.asList("2s", "15s", "15m", "Forever"); - ArrayAdapter arrayAdapter = new ArrayAdapter(this, android.R.layout.simple_spinner_item, arrayList); + ArrayAdapter arrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, arrayList); arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(arrayAdapter); spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @@ -143,10 +136,10 @@ public class MainActivity extends AppCompatActivity { }); // Populate page spinner - List pageList = new ArrayList(); + List pageList = new ArrayList<>(); for (int i = 0; i < 7; i++) pageList.add(Integer.toString(i)); - ArrayAdapter arrayAdapterPage = new ArrayAdapter(this, android.R.layout.simple_spinner_item, pageList); + ArrayAdapter arrayAdapterPage = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item, pageList); arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); binding.main.spinnerPage.setAdapter(arrayAdapterPage); binding.main.spinnerPage.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @@ -159,98 +152,77 @@ public class MainActivity extends AppCompatActivity { } }); - binding.main.buttonTXPageDM.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - // 0x85, 0x00, 0x00, 0x00, 0x00, 0x06, 0xF1, 0x00, 0x00, 0x00, 0x00 - List frames = new ArrayList(); - Byte[] payload = {0x06, 0x00, 0x00, 0x00, 0x00, 0x00}; - payload[1] = (byte)(((dispPage & 7) << 3) | 1); + binding.main.buttonTXPageDM.setOnClickListener(view -> { + // 0x85, 0x00, 0x00, 0x00, 0x00, 0x06, 0xF1, 0x00, 0x00, 0x00, 0x00 + List frames = new ArrayList<>(); + Byte[] payload = {0x06, 0x00, 0x00, 0x00, 0x00, 0x00}; + payload[1] = (byte)(((dispPage & 7) << 3) | 1); - if (dispDurationIdx != 3) { - int[] durations = {2, 15, 15*60, -1}; - int duration = durations[dispDurationIdx]; - payload[4] = (byte)(duration >> 8); - payload[5] = (byte)(duration & 255); - } else { - payload[1] = (byte)(payload[1] | 0x80); // "Forever" flag - } - - frames.add(new IRFrame(0, (byte)0x85, Arrays.asList(payload), 30, 200)); - IRTransmit(frames); - } - }); - - binding.main.buttonTXPage.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - // 0x84, 0x00, 0x00, 0x00, 0x00, 0xAB, 0x00, 0x00, 0x00 - - List frames = new ArrayList(); - Byte[] payload = {(byte)0xAB, 0x00, 0x00, 0x00}; - - int[] durations = {1, 3, 5, 0x80}; + if (dispDurationIdx < 3) { + // Warning: This must match contents of spinnerDuration ! + int[] durations = {2, 15, 15*60}; int duration = durations[dispDurationIdx]; - payload[1] = (byte)(((dispPage & 7) << 3) | duration); - - frames.add(new IRFrame(0, (byte)0x84, Arrays.asList(payload), 30, 200)); - IRTransmit(frames); + payload[4] = (byte)(duration >> 8); + payload[5] = (byte)(duration & 255); + } else { + payload[1] = (byte)(payload[1] | 0x80); // "Forever" flag } + + frames.add(new IRFrame(0, (byte)0x85, Arrays.asList(payload), 30, 200)); + IRTransmit(frames); }); - binding.main.switchLoopTX.setOnCheckedChangeListener(new Switch.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - loopTX = isChecked; - } + binding.main.buttonTXPage.setOnClickListener(view -> { + // 0x84, 0x00, 0x00, 0x00, 0x00, 0xAB, 0x00, 0x00, 0x00 + List frames = new ArrayList<>(); + Byte[] payload = {(byte)0xAB, 0x00, 0x00, 0x00}; + + // Warning: This must match contents of spinnerDuration ! + int[] durations = {1, 3, 5, 0x80}; + int duration = durations[dispDurationIdx]; + payload[1] = (byte)(((dispPage & 7) << 3) | duration); + + frames.add(new IRFrame(0, (byte)0x84, Arrays.asList(payload), 30, 200)); + IRTransmit(frames); }); - binding.main.switchTXAuto.setOnCheckedChangeListener(new Switch.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - autoTX = isChecked; - } - }); + // Loop mode checkbox + binding.main.switchLoopTX.setOnCheckedChangeListener((buttonView, isChecked) -> loopTX = isChecked); - binding.main.buttonTXImage.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - TransmitImage(); - } - }); + // Auto mode checkbox + binding.main.switchTXAuto.setOnCheckedChangeListener((buttonView, isChecked) -> autoTX = isChecked); + + binding.main.buttonTXImage.setOnClickListener(view -> TransmitImage()); // Image selection and processing ActivityResultLauncher someActivityResultLauncher = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), - new ActivityResultCallback() { - @Override - public void onActivityResult(ActivityResult result) { - if (result.getResultCode() == Activity.RESULT_OK) { - if (result.getData() != null) { - selectedImageUri = result.getData().getData(); - convertImage(); - } + result -> { + if (result.getResultCode() == Activity.RESULT_OK) { + if (result.getData() != null) { + selectedImageUri = result.getData().getData(); + convertImage(); } } }); - binding.main.buttonLoadImg.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); - photoPickerIntent.setType("image/*"); - photoPickerIntent.setAction(Intent.ACTION_GET_CONTENT); - someActivityResultLauncher.launch(photoPickerIntent); - } + binding.main.buttonLoadImg.setOnClickListener(view -> { + Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); + photoPickerIntent.setType("image/*"); + photoPickerIntent.setAction(Intent.ACTION_GET_CONTENT); + someActivityResultLauncher.launch(photoPickerIntent); }); // BW or BWR mode selection - binding.main.radiogroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(RadioGroup group, int checkedId) { - inBWR = binding.main.radioBWR.isChecked(); - updateImage(); - } + binding.main.radiogroup.setOnCheckedChangeListener((group, checkedId) -> { + inBWR = binding.main.radioBWR.isChecked(); + updateImagePreview(); + }); + + // Dithering selection + binding.main.radiogroup2.setOnCheckedChangeListener((group, checkedId) -> { + dithering = binding.main.radioDitherOn.isChecked(); + convertImage(); }); // Image scaling change @@ -269,8 +241,7 @@ public class MainActivity extends AppCompatActivity { }); // Start camera preview. Ask for camera permission if not already granted. - if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) - != PackageManager.PERMISSION_GRANTED){ + if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED){ ActivityCompat.requestPermissions(MainActivity.this, new String[] {Manifest.permission.CAMERA}, 123); } else { setScanPreview(); @@ -310,36 +281,56 @@ public class MainActivity extends AppCompatActivity { new DMConvert( binding.main.progressbar, - new AsyncResponse() { - @Override - public void processFinish(DMImage dmimage) { + dmimage -> { dmImage = dmimage; binding.main.textScale.setText(getString(R.string.image_dimensions, dmimage.bitmapBW.getWidth(), dmimage.bitmapBW.getHeight())); - updateImage(); - Log.d("PHX", "Converted image ok"); - } - }).execute(selectedImage); + updateImagePreview(); + }).execute(new DMConvertParams(selectedImage, dithering)); } catch (Exception e) { Log.d("PHX", e.getLocalizedMessage()); + Log.d("PHX", "TEST !!!!"); } } - public void updateImage() { + public void updateImagePreview() { if (dmImage != null) binding.main.imageviewDm.setImageBitmap(inBWR ? dmImage.bitmapBWR : dmImage.bitmapBW); } private Bitmap scaleImage(Bitmap selectedImage) { - float originalWidth = selectedImage.getWidth(); - float originalHeight = selectedImage.getHeight(); int newWidth, newHeight; - if (originalWidth < 300) { - //imageScale = (int)(originalWidth * 100) / 300; - newWidth = (int)originalWidth; - newHeight = (int)originalHeight; + int originalWidth = selectedImage.getWidth(); + int originalHeight = selectedImage.getHeight(); + + if ((originalWidth < 300) && (originalHeight < 300)) { + // If the original dimensions can plausibly fit in an existing ESL display, keep them + newWidth = originalWidth; + newHeight = originalHeight; + binding.main.seekScale.setEnabled(false); } else { + // Otherwise, use the slider scaling factor, with a resulting width of 300px when it's at max + // Kind of silly and won't work well if image is taller than it is wide, but good enough for now newWidth = (300 * imageScale) / 100; - newHeight = (int)(newWidth * (originalHeight / originalWidth)); + newHeight = (newWidth * (originalHeight / originalWidth)); + binding.main.seekScale.setEnabled(true); } + + // Perform the slightest possible resize to match the constraint of having a pixel count multiple of 8 + int pixelCount = newWidth * newHeight; + boolean multipleOfEight = (pixelCount & 7) == 0; + Log.d("PHX", String.format("pixelCount = %d (%smultiple of 8)", pixelCount, multipleOfEight ? "" : "not a ")); + if (!multipleOfEight) { + binding.main.textStatus2.setText("Adjusted image size for pixel count to be a multiple of 8."); + int nearestWidth = newWidth % 8; + int nearestHeight = newHeight % 8; + if (nearestWidth < nearestHeight) { + newWidth -= nearestWidth; + } else { + newHeight -= nearestHeight; + } + pixelCount = newWidth * newHeight; + Log.d("PHX", String.format("New pixelCount = %d (multiple of 8)", pixelCount)); + } + return Bitmap.createScaledBitmap(selectedImage, newWidth, newHeight, true); } @@ -349,22 +340,20 @@ public class MainActivity extends AppCompatActivity { transmitting = true; new ESLBlaster( usbSerialPort, + Character.getNumericValue(blasterHWVersion), binding.main.progressbar, binding.main.textStatus2, - new AsyncResponseTX() { - @Override - public void processFinish(boolean result) { + result -> { if (loopTX) { IRTransmit(frames); } else { transmitting = false; enableTXWidgets(true); } - } - }).execute(frames); + }).execute(frames); } - public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + public void onRequestPermissionsResult(int requestCode, @NotNull String[] permissions, @NotNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == 123) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { @@ -377,7 +366,7 @@ public class MainActivity extends AppCompatActivity { } } - Camera.PreviewCallback previewCb = new Camera.PreviewCallback() { + Camera.PreviewCallback previewCB = new Camera.PreviewCallback() { public void onPreviewFrame(byte[] data, Camera camera) { String strSupplement; Camera.Size size = camera.getParameters().getPreviewSize(); @@ -386,10 +375,10 @@ public class MainActivity extends AppCompatActivity { if (MainActivity.this.scanner.scanImage(previewimage) != 0) { Iterator it = MainActivity.this.scanner.getResults().iterator(); //while (it.hasNext()) { - String barcodeString = ((Symbol) it.next()).getData(); - if (barcodeString != lastBarcodeString) { + String barcodeString = (it.next()).getData(); + //if (!barcodeString.equals(lastBarcodeString)) { if (barcodeValid(barcodeString)) { - lastPLID = (Integer.parseInt(barcodeString.substring(2, 7))<<16) + Integer.parseInt(barcodeString.substring(7, 12)); + lastPLID = ((long) Integer.parseInt(barcodeString.substring(2, 7)) <<16) + Integer.parseInt(barcodeString.substring(7, 12)); String PLSerial = Long.toHexString(lastPLID); while (PLSerial.length() < 8) PLSerial = "0" + PLSerial; @@ -404,17 +393,15 @@ public class MainActivity extends AppCompatActivity { lastPLID = 0; } binding.main.textLastScanned.setText("Last scanned:\n" + barcodeString + "\n (" + strSupplement + ")"); - } - lastBarcodeString = barcodeString; + //} + //lastBarcodeString = barcodeString; } } }; - private Runnable doAutoFocus = new Runnable() { - public void run() { - /*if (MainActivity.this.mCamera != null) { - MainActivity.this.mCamera.autoFocus(MainActivity.this.autoFocusCB); - }*/ - } + private final Runnable doAutoFocus = () -> { + /*if (MainActivity.this.mCamera != null) { + MainActivity.this.mCamera.autoFocus(MainActivity.this.autoFocusCB); + }*/ }; Camera.AutoFocusCallback autoFocusCB = new Camera.AutoFocusCallback() { @@ -426,9 +413,9 @@ public class MainActivity extends AppCompatActivity { public void setScanPreview() { // Init Camera preview this.autoFocusHandler = new Handler(); - this.mPreview = new CameraPreview(this, this.previewCb, this.autoFocusCB); + CameraPreview mPreview = new CameraPreview(this, this.previewCB, this.autoFocusCB); this.preview = binding.main.cameraPreview; - this.preview.addView(this.mPreview); + this.preview.addView(mPreview); // Init ZBar this.scanner = new ImageScanner(); @@ -445,17 +432,17 @@ public class MainActivity extends AppCompatActivity { if (Integer.parseInt(barcodeString.substring(5, 6)) > 53) return false; int sum = 0; for (int i = 0; i < barcodeString.length() - 1; i++) - sum += (int)barcodeString.charAt(i); - if (sum % 10 != Integer.parseInt(barcodeString.substring(16, 17))) return false; - return true; + sum += barcodeString.charAt(i); + return sum % 10 == Integer.parseInt(barcodeString.substring(16, 17)); } public void status(String msg) { Snackbar.make(mainlayout, msg, Snackbar.LENGTH_LONG).setAction("Action", null).show(); } + @SuppressLint("DefaultLocale") public void testConnect() { - UsbDevice device = null; + UsbDevice device; UsbManager usbManager = (UsbManager) getSystemService(Context.USB_SERVICE); /*for(UsbDevice v : usbManager.getDeviceList().values()) if(v.getDeviceId() == deviceId) @@ -473,7 +460,7 @@ public class MainActivity extends AppCompatActivity { status("ESL Blaster connection failed: no driver found"); return; } - if (driver.getPorts().size() < 0) { + if (driver.getPorts().size() < 1) { status("ESL Blaster connection failed: not enough ports at device"); return; } @@ -504,8 +491,8 @@ public class MainActivity extends AppCompatActivity { usbSerialPort.write(data, 1000); if (usbSerialPort.read(buffer, 2000) > 0) { // 2s timeout should be enough String s = new String(buffer, StandardCharsets.UTF_8).substring(0, 12); - Log.d("PHX", String.valueOf((char)buffer[0])); - if (s.substring(0, 10).equals("ESLBlaster")) { + //Log.d("PHX", String.valueOf((char)buffer[0])); + if (s.startsWith("ESLBlaster")) { status("ESL Blaster connected !"); blasterHWVersion = s.charAt(10); blasterFWVersion = Character.digit(s.charAt(11), 10); @@ -513,7 +500,7 @@ public class MainActivity extends AppCompatActivity { enableTXWidgets(true); binding.main.textStatus.setText(String.format("ESLBlaster connected (HW %c, FW %d)", blasterHWVersion, blasterFWVersion)); } - }; + } } catch(IOException e) { e.printStackTrace(); setDisconnected(); diff --git a/PriceHax/app/src/main/res/layout/content_main.xml b/PriceHax/app/src/main/res/layout/content_main.xml index 0fffb87..0fac108 100644 --- a/PriceHax/app/src/main/res/layout/content_main.xml +++ b/PriceHax/app/src/main/res/layout/content_main.xml @@ -5,6 +5,18 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> + +