mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Initial support for thermal printers
This commit is contained in:
committed by
daniele
parent
0996b5d810
commit
07d0895c06
@@ -264,6 +264,10 @@
|
||||
android:name=".devices.ultrahuman.UltrahumanBreathingActivity"
|
||||
android:label="@string/devicetype_ultrahuma_ring_air"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".devices.thermalprinter.SendToPrinterActivity"
|
||||
android:label="@string/devicetype_generic_thermal_printer"
|
||||
android:parentActivityName=".activities.ControlCenterv2" />
|
||||
<activity
|
||||
android:name=".activities.FwAppInstallerActivity"
|
||||
android:label="@string/title_activity_fw_app_insaller"
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:svg="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
height="28.740126"
|
||||
id="svg2"
|
||||
version="1.1" viewBox="0 0 28.740126 28.740126"
|
||||
width="28.740126" xmlns="http://www.w3.org/2000/svg"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)" sodipodi:docname="bluetooth_printer.svg">
|
||||
<defs id="defs6">
|
||||
<inkscape:perspective id="perspective903" inkscape:persp3d-origin="14.370063 : 9.580042 : 1"
|
||||
inkscape:vp_x="0 : 14.370063 : 1" inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="28.740126 : 14.370063 : 1" sodipodi:type="inkscape:persp3d" />
|
||||
</defs>
|
||||
<g style="display:inline" id="layer1" inkscape:groupmode="layer" inkscape:label="Background">
|
||||
<circle
|
||||
style="fill:none;fill-opacity:0.846154;stroke:#ffffff;stroke-width:1.04987;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
cx="14.370063" cy="19.524071" id="path1229" r="2.2537029" />
|
||||
<g style="display:inline" id="layer3" inkscape:groupmode="layer" inkscape:label="Middle"
|
||||
sodipodi:insensitive="true">
|
||||
<rect
|
||||
style="display:inline;fill:#4dabf5;fill-opacity:1;stroke:none;stroke-width:3.57115;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
height="21.904638" id="rect817-7" ry="0.9472276" width="22.819572" x="2.9317887"
|
||||
y="3.0349438" />
|
||||
</g>
|
||||
<g style="display:inline" id="layer2" inkscape:groupmode="layer" inkscape:label="Top">
|
||||
<rect
|
||||
style="fill:#2196f3;fill-opacity:1;stroke:none;stroke-width:3.57115;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
height="21.904638" id="rect817" ry="0.9472276" width="22.819572" x="2.9238882"
|
||||
y="3.4130721" />
|
||||
</g>
|
||||
<g style="fill:#fffffe;fill-opacity:1;stroke:none;stroke-width:1.17455" id="g870"
|
||||
transform="matrix(0.851388,0,0,0.851388,4.1035946,4.1325617)" />
|
||||
<g style="display:inline" id="layer4" inkscape:groupmode="layer" inkscape:label="Bottom"
|
||||
sodipodi:insensitive="true">
|
||||
<rect
|
||||
style="display:inline;fill:#1f7fdb;fill-opacity:1;stroke:none;stroke-width:3.57115;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
height="21.904638" id="rect817-5" ry="0.9472276" width="22.819572" x="2.9239759"
|
||||
y="3.8768768" />
|
||||
</g>
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#ffffff;stroke-width:0.524934;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:2.09974, 1.04987;stroke-dashoffset:0;stroke-opacity:1"
|
||||
d="M 4.2838873,14.928158 H 24.456238" id="path1465" inkscape:label="path1465" />
|
||||
<rect
|
||||
style="fill:none;stroke:#ffffff;stroke-width:1.2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke;stop-color:#000000"
|
||||
height="19" id="rect846" ry="5.7287159" width="20.062387" x="4.3388691" y="4.8700628" />
|
||||
</g>
|
||||
<g style="display:none" id="layer5" inkscape:groupmode="layer" inkscape:label="Original">
|
||||
<image style="opacity:0.490741;image-rendering:optimizeQuality" height="28.710279" id="image4124"
|
||||
preserveAspectRatio="none"
|
||||
width="28.710279" x="0.010951031"
|
||||
y="0.0079936879" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAA3NCSVQICAjb4U/gAAAACXBIWXMA ADFOAAAxTgGacD5yAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAABF0RVh0 VGl0bGUAUGViYmxlIEljb26KvwD9AAAADHRFWHRBdXRob3IAeHBobnh/4OzMAAAAY3RFWHRDb3B5 cmlnaHQAQ0MgQXR0cmlidXRpb24tTm9uQ29tbWVyY2lhbC1TaGFyZUFsaWtlIGh0dHA6Ly9jcmVh dGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LW5jLXNhLzMuMC/eBBrlAAABy1BMVEUAAAAAAAAA AP8AAIAAAFUAAKoAVaoAAIAAQIAAM2YAM5kzM2YzM5krK4AkJG0kJJIgIIAcHHEcHI4aGoAXF3QX F4sXLnQXLosVK4AUJ3YUJ4kSJIAkJIARInciInciIogQIIAgIIAeHngeHoccHIAcK4AbKHkbKIYa GoAaJoAYJHkXI4AXI4sWIXoWIYUVIIAfH3ofH4UdJ4ATHIQcJnsbJIAaI4QaIoAZIXsZIYQYIHgY IIAXH4MXJ3wXHoAXJoBLpfAWJHwdJHwdJIMcI4Acas0VIoMbIoAaIXwaIIAZH3wZH4MZJXwYHoAY HoYYJIAehtsYHoIefdtHoO0jetZGovNEn+4gg+EfgN5Ep/VKqfREpvQgfdkgfttKqvQggd4hjesw nPMwnPRJqfUhgd4hjutJqfUgfttNq/VNq/QhlPEhlPIhlvMkmPMglvIff9sgjeoglPAhk/AhlPEh lPIhlvMkl/MkmPMlmPMmmPMnmfMpmvMzn/Q0n/Q1n/Q2oPRIqfVNq/VasfZbsvZhtPZitfdtuveQ yvmXzvmj0/qk1Pqu2fu83/vR6f3T6v3Z7f3i8f3j8v3l8/7m8/7p9f7q9f7x+P7y+f7////ougeB AAAAb3RSTlMAAQECAwMDBAQFBQUFBgcHCAkJCgsLCwsMDQ0ODg8PDxAQERESEhMTFBQVFhYXFxgZ GRobGxwdHh8fICAhISIiIiMjIyQkJSYnKCkpKSoqKiorKyssLC2xs7O0tba2tu7u7u7u7+/w8fHy +/v7+/ywJhX9AAAC0klEQVRo3u2a+VfTQBCAm00iW0nSTVNKbKQYbIMUC6jU4q2IB2rF+74vPEAR LSjIIXgAAiqef65py6MNJe17y1RF8/26L/N1N9O8ZGZcLof/DiYFoiV1cZHwiGVZjh7zalRAYYbn sFsQ6RFwGccyBeJjwSMrPn8lHX6fQqRy3tbAsFiU1So9FA4bNIRD63XNTwTO5pSYc9fuvwbgwaW9 SxqYs/eGBgcAGBy+fRrlpRPDsJdfJoEYuMgvzibz/vK3kmDcFMuxNZvM/BG74QTdFV6PJZsYlhe9 XXCCrlCVStwcyhG45QCkoCmiV0o4uwXEib4aQMHjbc0RTXYvCMx/mGdNHaSgraWxRhEXzsgUEC0C KWjfszlUYRWsrYcUHGttNlSRzxUEo5CCjrZYrSqVVlD3Nwj6vqcZsovzKrPeTy3o/5lmxE4wnFl/ 4QhKLvjy2YY5IEExHMH0B1umQQSj9o+EUUewIBh/m8c4qOBbfu58dQRLCmYnJmZLKphKJicdwR8X vJsymSmhIM37FSv4OJlhxnlUOAJHAC8Ye5PHmPNe9PsEJX/5dT5AigvmPtkw53wnr3xB34809uWc zDp9OWflV7xgBaBlzY79MWORALhuui+vbhoArfwe3rUp5MtWfl2Il1QDUtAaj+rKakv1XdEhBfGm Wo1kq+/pM1Ih+wdRI6iIq3IFvEAewgke6QHFg1mU26PjhbtwghsKEbG1X8ew3PmnUPGfXRAwv7hr yqAD13t6nwPQ23PnBMeivG4gg85c7XwCQOeVU+avZ5bslcrrNm7dfehIInGSguOJo+0Hd27ZoOam j3ULvOjTI02x+PYddLTEY42GJpfnpo/L0g50k8pqI1IfbWikoSFaXxfSFKnMrqFsZpKbKKoWDFbT EdQCqlfCdhtIG7AoESLTQogk4EJd/VRTn8fLgecKTg3MjyUsA4SKDD7MT1bQ8++NmvwCRmydh2Fg UvEAAAAASUVORK5CYII= " />
|
||||
</g>
|
||||
<metadata id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview bordercolor="#666666" borderopacity="1" gridtolerance="10"
|
||||
guidetolerance="10" id="namedview4" objecttolerance="10" pagecolor="#ffffff"
|
||||
showgrid="false" showguides="true" inkscape:current-layer="layer1"
|
||||
inkscape:cx="7.711883" inkscape:cy="13.545514" inkscape:deskcolor="#d1d1d1" inkscape:guide-bbox="true"
|
||||
inkscape:pagecheckerboard="0" inkscape:pageopacity="0" inkscape:pageshadow="2"
|
||||
inkscape:showpageshadow="2" inkscape:window-height="1365" inkscape:window-maximized="1"
|
||||
inkscape:window-width="2560" inkscape:window-x="0" inkscape:window-y="0"
|
||||
inkscape:zoom="22.627418">
|
||||
<inkscape:grid id="grid972" type="xygrid" />
|
||||
</sodipodi:namedview>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
+65
@@ -0,0 +1,65 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.thermalprinter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.InstallActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
|
||||
|
||||
public class ImageFilePrinterHandler implements InstallHandler {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ImageFilePrinterHandler.class);
|
||||
|
||||
protected final Context mContext;
|
||||
private Bitmap incomingBitmap;
|
||||
|
||||
public ImageFilePrinterHandler(final Uri uri, final Context context) {
|
||||
this.mContext = context;
|
||||
|
||||
final UriHelper uriHelper;
|
||||
try {
|
||||
uriHelper = UriHelper.get(uri, context);
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to get uri", e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
incomingBitmap = BitmapFactory.decodeStream(uriHelper.openInputStream());
|
||||
} catch (FileNotFoundException e) {
|
||||
LOG.error("Failed to create bitmap", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Bitmap getIncomingBitmap() {
|
||||
return incomingBitmap;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return incomingBitmap != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateInstallation(InstallActivity installActivity, GBDevice device) {
|
||||
|
||||
installActivity.setPreview(incomingBitmap);
|
||||
installActivity.setInstallEnabled(true);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartInstall(GBDevice device) {
|
||||
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.thermalprinter;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import com.google.android.material.materialswitch.MaterialSwitch;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.thermalprinter.BitmapToBitSet;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.thermalprinter.GenericThermalPrinterSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.UriHelper;
|
||||
|
||||
|
||||
public class SendToPrinterActivity extends AbstractGBActivity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendToPrinterActivity.class);
|
||||
|
||||
private Bitmap bitmap;
|
||||
private ImageView previewImage;
|
||||
private MaterialSwitch dithering;
|
||||
|
||||
private BitmapToBitSet bitmapToBitSet;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_print_image);
|
||||
ImageView incomingImage = findViewById(R.id.incomingImage);
|
||||
previewImage = findViewById(R.id.convertedImage);
|
||||
Button sendToPrinter = findViewById(R.id.sendToPrinterButton);
|
||||
dithering = findViewById(R.id.switchDithering);
|
||||
final TextView warning = findViewById(R.id.warning_devices);
|
||||
|
||||
final List<GBDevice> devices = ((GBApplication) getApplicationContext()).getDeviceManager().getSelectedDevices();
|
||||
GBDevice device = devices.get(0);
|
||||
|
||||
switch (devices.size()) {
|
||||
case 0:
|
||||
warning.setText(R.string.open_fw_installer_connect_minimum_one_device);
|
||||
sendToPrinter.setEnabled(false);
|
||||
break;
|
||||
case 1:
|
||||
warning.setText(String.format(getString(R.string.open_fw_installer_select_file), device.getAliasOrName()));
|
||||
sendToPrinter.setEnabled(true);
|
||||
break;
|
||||
default:
|
||||
warning.setText(R.string.open_fw_installer_connect_maximum_one_device);
|
||||
sendToPrinter.setEnabled(false);
|
||||
}
|
||||
|
||||
dithering.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
LOG.info("dithering is : {}", dithering.isChecked());
|
||||
bitmapToBitSet.toBlackAndWhite(dithering.isChecked());
|
||||
|
||||
previewImage.setImageBitmap(bitmapToBitSet.getPreview());
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Uri uri = getIntent().getData();
|
||||
if (uri == null) { // For "share" intent
|
||||
uri = getIntent().getParcelableExtra(GenericThermalPrinterSupport.INTENT_EXTRA_URI);
|
||||
}
|
||||
|
||||
final UriHelper uriHelper;
|
||||
try {
|
||||
uriHelper = UriHelper.get(uri, getApplicationContext());
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to get uri", e);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final Bitmap incoming = BitmapFactory.decodeStream(uriHelper.openInputStream());
|
||||
if (incoming.getWidth() > 384) {
|
||||
float aspectRatio = (float) incoming.getHeight() / incoming.getWidth();
|
||||
bitmap = Bitmap.createScaledBitmap(incoming, 384, (int) (384 * aspectRatio), true);
|
||||
} else {
|
||||
bitmap = incoming;
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
LOG.error("Failed to create bitmap", e);
|
||||
}
|
||||
incomingImage.setImageBitmap(bitmap);
|
||||
|
||||
bitmapToBitSet = new BitmapToBitSet(bitmap);
|
||||
bitmapToBitSet.toBlackAndWhite(dithering.isChecked());
|
||||
|
||||
previewImage.setImageBitmap(bitmapToBitSet.getPreview());
|
||||
|
||||
sendToPrinter.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
sendToPrinter();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sendToPrinter() {
|
||||
Intent intent = new Intent(GenericThermalPrinterSupport.INTENT_ACTION_PRINT_BITMAP);
|
||||
//TODO: this is horrible
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||
intent.putExtra(GenericThermalPrinterSupport.INTENT_EXTRA_BITMAP, stream.toByteArray());
|
||||
|
||||
intent.putExtra(GenericThermalPrinterSupport.INTENT_EXTRA_APPLY_DITHERING, dithering.isChecked());
|
||||
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
|
||||
}
|
||||
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.thermalprinter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.thermalprinter.GenericThermalPrinterSupport;
|
||||
|
||||
public class ThermalPrinterCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_LAZY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDefaultIconResource() {
|
||||
return R.drawable.ic_device_bluetooth_printer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass(GBDevice gbDevice) {
|
||||
return GenericThermalPrinterSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_generic_thermal_printer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(GBDeviceCandidate candidate) {
|
||||
return candidate.supportsService(GenericThermalPrinterSupport.discoveryService);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFlashing() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addBatteryPollingSettings() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
//TODO: maybe there is another way to implement opening/receiving pictures
|
||||
final ImageFilePrinterHandler imageFilePrinterHandler = new ImageFilePrinterHandler(uri, context);
|
||||
if (imageFilePrinterHandler.isValid()) {
|
||||
Intent instentStartPrintActivity = new Intent(context, SendToPrinterActivity.class);
|
||||
instentStartPrintActivity.putExtra(GenericThermalPrinterSupport.INTENT_EXTRA_URI, uri);
|
||||
context.startActivity(instentStartPrintActivity);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -329,6 +329,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.motion300.Soundcor
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.q30.SoundcoreQ30Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.supercars.SuperCarsCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.thermalprinter.ThermalPrinterCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.tlw64.TLW64Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.um25.Coordinator.UM25Coordinator;
|
||||
@@ -731,6 +732,7 @@ public enum DeviceType {
|
||||
BLE_GATT_CLIENT(BleGattClientCoordinator.class),
|
||||
MARSTEK_B2500(MarstekB2500DeviceCoordinator.class),
|
||||
GENERIC_HEADPHONES(GenericHeadphonesCoordinator.class),
|
||||
GENERIC_THERMAL_PRINTER(ThermalPrinterCoordinator.class),
|
||||
TEST(TestDeviceCoordinator.class),
|
||||
ULTRAHUMAN_RING_AIR(UltrahumanDeviceCoordinator.class),
|
||||
ATC_BLE_OEPL(ATCBLEOEPLCoordinator.class);
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.thermalprinter;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
|
||||
public class BitmapToBitSet {
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
private final int width;
|
||||
private final int height;
|
||||
private final Bitmap bitmap;
|
||||
private final BitSet bwPixels;
|
||||
private int threshold = 128;
|
||||
|
||||
public BitmapToBitSet(Bitmap bitmap) {
|
||||
this.bitmap = bitmap;
|
||||
this.width = bitmap.getWidth();
|
||||
this.height = bitmap.getHeight();
|
||||
this.bwPixels = new BitSet(this.width * this.height);
|
||||
}
|
||||
|
||||
public int getThreshold() {
|
||||
return threshold;
|
||||
}
|
||||
|
||||
public void setThreshold(int threshold) {
|
||||
this.threshold = threshold;
|
||||
}
|
||||
|
||||
public Bitmap getPreview() {
|
||||
final Bitmap bwBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
final int[] pixels = new int[width * height];
|
||||
Arrays.fill(pixels, Color.WHITE);
|
||||
|
||||
for (int i = bwPixels.nextSetBit(0); i >= 0; i = bwPixels.nextSetBit(i + 1)) {
|
||||
pixels[i] = Color.BLACK;
|
||||
}
|
||||
|
||||
bwBitmap.setPixels(pixels, 0, width, 0, 0, width, height);
|
||||
return bwBitmap;
|
||||
}
|
||||
|
||||
public BitSet toBlackAndWhite(final boolean applyDithering) {
|
||||
bwPixels.clear();
|
||||
int[] pixels = new int[width * height];
|
||||
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int index = y * width + x;
|
||||
int currentPixelColor = pixels[index];
|
||||
|
||||
if (Color.alpha(currentPixelColor) == 0) {
|
||||
pixels[index] = Color.WHITE; //flatten transparent pixels to white
|
||||
continue;
|
||||
}
|
||||
|
||||
int currentPixelGrayscale = getGrayscaleValue(currentPixelColor);
|
||||
boolean isBlack = (currentPixelGrayscale < threshold);
|
||||
|
||||
if (isBlack)
|
||||
bwPixels.set(index);
|
||||
|
||||
if (applyDithering) {
|
||||
int bwResultingPixel = (isBlack) ? 0 : 255;
|
||||
|
||||
pixels[index] = Color.argb(Color.alpha(currentPixelColor), bwResultingPixel, bwResultingPixel, bwResultingPixel);
|
||||
|
||||
int quantError = currentPixelGrayscale - bwResultingPixel;
|
||||
|
||||
// Apply dithering to neighboring pixels
|
||||
if (x + 1 < width) {
|
||||
applyErrorToPixel(pixels, y * width + (x + 1), quantError * 7 / 16);
|
||||
}
|
||||
if (x - 1 >= 0 && y + 1 < height) {
|
||||
applyErrorToPixel(pixels, (y + 1) * width + (x - 1), quantError * 3 / 16);
|
||||
}
|
||||
if (y + 1 < height) {
|
||||
applyErrorToPixel(pixels, (y + 1) * width + x, quantError * 5 / 16);
|
||||
}
|
||||
if (x + 1 < width && y + 1 < height) {
|
||||
applyErrorToPixel(pixels, (y + 1) * width + (x + 1), quantError / 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bwPixels;
|
||||
}
|
||||
|
||||
private void applyErrorToPixel(int[] pixels, int index, int error) {
|
||||
int currentPixelColor = pixels[index];
|
||||
final int newGray = Math.max(0, Math.min(255, getGrayscaleValue(currentPixelColor) + error));
|
||||
pixels[index] = Color.argb(Color.alpha(currentPixelColor), newGray, newGray, newGray);
|
||||
}
|
||||
|
||||
private int getGrayscaleValue(int color) {
|
||||
return (int) (0.3 * Color.red(color) + 0.59 * Color.green(color) + 0.11 * Color.blue(color));
|
||||
}
|
||||
|
||||
}
|
||||
+472
@@ -0,0 +1,472 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.thermalprinter;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.BufferOverflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.BitSet;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class GenericThermalPrinterSupport extends AbstractBTLESingleDeviceSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(GenericThermalPrinterSupport.class);
|
||||
public static final String INTENT_ACTION_PRINT_BITMAP = "print_bitmap";
|
||||
public static final String INTENT_EXTRA_URI = "picture_uri";
|
||||
public static final String INTENT_EXTRA_BITMAP = "picture";
|
||||
public static final String INTENT_EXTRA_APPLY_DITHERING = "apply_dithering";
|
||||
|
||||
public static final UUID discoveryService = UUID.fromString("0000af30-0000-1000-8000-00805f9b34fb");
|
||||
private final UUID writeCharUUID = UUID.fromString("0000ae01-0000-1000-8000-00805f9b34fb");
|
||||
private final UUID notifCharUUID = UUID.fromString("0000ae02-0000-1000-8000-00805f9b34fb");
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private final Runnable batteryRunner = () -> {
|
||||
LOG.info("Running retrieving battery through runner.");
|
||||
send("getDevState", PrinterCommand.getDevState.message(new byte[]{0x00}));
|
||||
};
|
||||
private boolean useRunLengthEncoding = false;
|
||||
private boolean canPrint = false;
|
||||
|
||||
private final int IMAGE_WIDTH = 384;
|
||||
private final int PRINT_INTENSITY = 8000;
|
||||
private final int PRINT_SPEED = 10;
|
||||
private final int PRINT_TYPE = 0; //1 also observed
|
||||
|
||||
public GenericThermalPrinterSupport() {
|
||||
super(LOG);
|
||||
|
||||
addSupportedService(UUID.fromString("0000ae30-0000-1000-8000-00805f9b34fb"));
|
||||
|
||||
IntentFilter commandFilter = new IntentFilter();
|
||||
commandFilter.addAction(INTENT_ACTION_PRINT_BITMAP);
|
||||
BroadcastReceiver commandReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
switch (intent.getAction()) {
|
||||
case INTENT_ACTION_PRINT_BITMAP:
|
||||
byte[] bitmapData = intent.getByteArrayExtra(INTENT_EXTRA_BITMAP);
|
||||
if (bitmapData != null) {
|
||||
Bitmap bitmap = BitmapFactory.decodeByteArray(bitmapData, 0, bitmapData.length);
|
||||
boolean dither = intent.getBooleanExtra(INTENT_EXTRA_APPLY_DITHERING, false);
|
||||
printImage(bitmap, dither);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
LocalBroadcastManager.getInstance(GBApplication.getContext()).registerReceiver(commandReceiver, commandFilter);
|
||||
|
||||
}
|
||||
|
||||
private static byte[] byteBufferToArray(final ByteBuffer input) {
|
||||
input.flip();
|
||||
final byte[] result = new byte[input.limit()];
|
||||
input.get(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private static int getCrc8(final byte[] seq) {
|
||||
int crc = 0x00;
|
||||
|
||||
for (byte b : seq) {
|
||||
crc ^= (b & 0xFF);
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if ((crc & 0x80) != 0) {
|
||||
crc = (crc << 1) ^ 0x07;
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (byte) (crc & 0xFF);
|
||||
}
|
||||
|
||||
private byte[] encodePictureToPrinterCommands(BitSet imageBits, int imageWidth) {
|
||||
final int maxOctets = imageWidth / 8;
|
||||
final ByteBuffer result = ByteBuffer.allocate(imageBits.length() + 1000);
|
||||
|
||||
for (int row = 0; row < (imageBits.length() / imageWidth); row++) {
|
||||
boolean rowRLE = useRunLengthEncoding;
|
||||
final ByteBuffer rowContent = ByteBuffer.allocate(maxOctets);
|
||||
final BitSet rowBits = imageBits.get(row * imageWidth, row * imageWidth + imageWidth);
|
||||
if (useRunLengthEncoding) {
|
||||
try {
|
||||
rowContent.put(EncodingUtils.encodeRowRLE(rowBits, maxOctets));
|
||||
} catch (BufferOverflowException e) {
|
||||
//if compressed data is bigger than uncompressed, we use uncompressed
|
||||
rowRLE = false;
|
||||
rowContent.clear();
|
||||
rowContent.put(EncodingUtils.encodeRowPlain(rowBits));
|
||||
}
|
||||
} else {
|
||||
rowContent.put(EncodingUtils.encodeRowPlain(rowBits));
|
||||
}
|
||||
|
||||
result.put(rowRLE ? PrinterCommand.printRowRLE.message(byteBufferToArray(rowContent)) :
|
||||
PrinterCommand.printRowPlain.message(byteBufferToArray(rowContent))
|
||||
);
|
||||
}
|
||||
return byteBufferToArray(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void printImage(Bitmap bitmap, boolean applyDithering) {
|
||||
if (!canPrint) {
|
||||
LOG.error("Printer cannot print.");
|
||||
return;
|
||||
}
|
||||
final BitmapToBitSet bitmapToBitSet = new BitmapToBitSet(bitmap);
|
||||
|
||||
final ByteBuffer result = ByteBuffer.allocate(500 + (Math.round(bitmap.getWidth() / 8) + 8) * bitmap.getHeight()); //TODO: approximate better?
|
||||
|
||||
result.put(PrinterCommand.intensity(PRINT_INTENSITY));
|
||||
result.put(PrinterCommand.printSpeed.message(new byte[]{PRINT_SPEED}));
|
||||
result.put(PrinterCommand.printType.message(new byte[]{PRINT_TYPE}));
|
||||
result.put(PrinterCommand.quality(5));
|
||||
|
||||
result.put(encodePictureToPrinterCommands(bitmapToBitSet.toBlackAndWhite(applyDithering), bitmapToBitSet.getWidth()));
|
||||
result.put(PrinterCommand.feedPaper(72));
|
||||
|
||||
result.put(PrinterCommand.getDevState.message(new byte[]{0})); //to get back the current device status
|
||||
|
||||
send("Print...", byteBufferToArray(result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) {
|
||||
if (super.onCharacteristicChanged(gatt, characteristic, value)) {
|
||||
return true;
|
||||
}
|
||||
ByteBuffer data = ByteBuffer.wrap(value);
|
||||
|
||||
if (data.get() != PrinterCommand.preamble[0] || data.get() != PrinterCommand.preamble[1]) {
|
||||
LOG.error("Incoming message does not start with preamble: {}", data.array());
|
||||
return true;
|
||||
}
|
||||
|
||||
final int cmdId = data.get();
|
||||
PrinterCommand command = PrinterCommand.fromId(cmdId);
|
||||
if (command == null) {
|
||||
LOG.error("Unkwnown incoming command{} in message: {}", cmdId, GB.hexdump(data.array()));
|
||||
return true;
|
||||
}
|
||||
int response = data.get();
|
||||
if (0x01 != response) {
|
||||
LOG.error("Incoming message is not a response: {}", GB.hexdump(data.array()));
|
||||
return true;
|
||||
}
|
||||
int payloadLength = data.get();
|
||||
if (data.remaining() < payloadLength + 3) {
|
||||
LOG.error("Incoming message is incomplete: {}", GB.hexdump(data.array()));
|
||||
return true;
|
||||
}
|
||||
data.get();
|
||||
byte[] payload = new byte[payloadLength];
|
||||
data.get(payload);
|
||||
int checksum = data.get();
|
||||
if (checksum != getCrc8(payload)) {
|
||||
LOG.error("Incoming message CRC error does not match: {}", GB.hexdump(data.array()));
|
||||
return true;
|
||||
}
|
||||
if (((byte) 0xff) != data.get()) {
|
||||
LOG.error("Incoming message does not have correct terminator: {}", GB.hexdump(data.array()));
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG.debug("Incoming message: {}", GB.hexdump(data.array()));
|
||||
final ByteBuffer payloadBB = ByteBuffer.wrap(payload);
|
||||
switch (command) {
|
||||
case getDevInfo:
|
||||
if (payloadBB.get() != 0) {
|
||||
LOG.info("Setting the device supports Run Length Encoding");
|
||||
this.useRunLengthEncoding = true;
|
||||
} else {
|
||||
LOG.info("Device does not support Run Length Encoding");
|
||||
}
|
||||
payloadBB.get();//unk
|
||||
payloadBB.get();//unk
|
||||
byte[] version = new byte[payloadBB.remaining()];
|
||||
payloadBB.get(version);
|
||||
String versionString = new String(version);
|
||||
LOG.info("Device version(??): {}", versionString);
|
||||
return true;
|
||||
case getDevState:
|
||||
//5178A3010300 000E28 0EFF observed values
|
||||
//5178A3010300 000226 D8FF
|
||||
//5178A3010300 000526 B3FF
|
||||
//5178A3010300 000C25 07FF
|
||||
//5178A3010300 000D24 15FF
|
||||
//5178A3010300 080D23 51FF (here starts blinking red)
|
||||
int status = payloadBB.get();
|
||||
if (status == 0) {
|
||||
LOG.info("Enabling print");
|
||||
canPrint = true;
|
||||
} else if (status == 1) {
|
||||
GB.toast("Printer out of paper", Toast.LENGTH_LONG, GB.ERROR);
|
||||
canPrint = false;
|
||||
} else if (status == 8) {
|
||||
GB.toast("Printer battery almost empty", Toast.LENGTH_LONG, GB.ERROR);
|
||||
canPrint = true;
|
||||
} else {
|
||||
LOG.warn("Status != 0, disabling print functionality. Status: {}", status);
|
||||
canPrint = false;
|
||||
}
|
||||
if (payloadBB.hasRemaining()) {
|
||||
int unk = payloadBB.get();
|
||||
int battery = payloadBB.get(); //possibly
|
||||
|
||||
final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
|
||||
batteryCmd.level = ((short) battery - 35) * 18 + 10;
|
||||
handleGBDeviceEvent(batteryCmd);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
LOG.info("Incoming message: {}", GB.hexdump(data.array()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
|
||||
builder.setCallback(this);
|
||||
getDevice().setFirmwareVersion("N/A");
|
||||
getDevice().setFirmwareVersion2("N/A");
|
||||
builder.requestMtu(512);
|
||||
builder.notify(getCharacteristic(notifCharUUID), true);
|
||||
|
||||
builder.write(getCharacteristic(writeCharUUID), PrinterCommand.getDevInfo.message(new byte[]{0x00}));
|
||||
|
||||
builder.write(getCharacteristic(writeCharUUID), PrinterCommand.getDevState.message(new byte[]{0x00}));
|
||||
|
||||
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
|
||||
|
||||
LOG.debug("Connected to: " + gbDevice.getName());
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public void send(String taskname, byte[] command) {
|
||||
TransactionBuilder builder = new TransactionBuilder(taskname);
|
||||
builder.writeChunkedData(getCharacteristic(writeCharUUID), command, 123);
|
||||
builder.queue(getQueue());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
// send(PrinterCommand.feedPaper.message(new byte[]{1}));
|
||||
sendTestPrint();
|
||||
}
|
||||
|
||||
private void sendTestPrint() {
|
||||
Bitmap bitmap = BitmapFactory.decodeResource(GBApplication.app().getResources(), R.drawable.ic_launcher);
|
||||
|
||||
Bitmap newBitmap = Bitmap.createBitmap(IMAGE_WIDTH, bitmap.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(newBitmap);
|
||||
canvas.drawColor(Color.WHITE);
|
||||
|
||||
// Calculate the top-left coordinates for centering the original Bitmap
|
||||
int posLeft = 0;
|
||||
int posMiddle = (newBitmap.getWidth() - bitmap.getWidth()) / 2;
|
||||
int posRight = newBitmap.getWidth() - bitmap.getWidth();
|
||||
|
||||
int left = posRight;
|
||||
int top = (newBitmap.getHeight() - bitmap.getHeight()) / 2;
|
||||
|
||||
canvas.drawBitmap(bitmap, left, top, new Paint());
|
||||
|
||||
canvas.drawText(ZonedDateTime.now().toString(), newBitmap.getHeight(), 0, new Paint());
|
||||
|
||||
printImage(newBitmap, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
switch (config) {
|
||||
case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_ENABLE:
|
||||
if (!GBApplication.getDevicePrefs(gbDevice).getBatteryPollingEnabled()) {
|
||||
stopBatteryRunnerDelayed();
|
||||
break;
|
||||
}
|
||||
// Fall through if enabled
|
||||
case DeviceSettingsPreferenceConst.PREF_BATTERY_POLLING_INTERVAL:
|
||||
if (!startBatteryRunnerDelayed()) {
|
||||
GB.toast(getContext(), R.string.battery_polling_failed_start, Toast.LENGTH_SHORT, GB.ERROR);
|
||||
LOG.error("Failed to start the battery polling");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean startBatteryRunnerDelayed() {
|
||||
int interval_minutes = GBApplication.getDevicePrefs(gbDevice).getBatteryPollingIntervalMinutes();
|
||||
int interval = interval_minutes * 60 * 1000;
|
||||
LOG.debug("Starting battery runner delayed by {} ({} minutes)", interval, interval_minutes);
|
||||
handler.removeCallbacks(batteryRunner);
|
||||
return handler.postDelayed(batteryRunner, interval);
|
||||
}
|
||||
|
||||
public void stopBatteryRunnerDelayed() {
|
||||
LOG.debug("Stopping battery runner delayed");
|
||||
handler.removeCallbacks(batteryRunner);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstallApp(Uri uri) {
|
||||
//we just abuse the install functionality to start our own activity
|
||||
}
|
||||
|
||||
private enum PrinterCommand {
|
||||
feedPaper(0xa1),
|
||||
printRowPlain(0xa2),
|
||||
getDevState(0xa3),
|
||||
quality(0xa4),
|
||||
printHeadSetup(0xa6), //possibly
|
||||
getDevInfo(0xa8),
|
||||
intensity(0xaf),
|
||||
printSpeed(0xbd),
|
||||
printType(0xbe),
|
||||
printRowRLE(0xbf),
|
||||
;
|
||||
private final int cmdId;
|
||||
public static final byte[] preamble = new byte[]{0x51, 0x78};
|
||||
|
||||
//Following messages observed in dumps before and after the actual print, seem to make no difference hence we're not sending them ATM
|
||||
public static final byte[] setupPrintBegin = printHeadSetup.message(new byte[]{-86, 85, 23, 56, 68, 95, 95, 95, 68, 56, 44});
|
||||
public static final byte[] setupPrintEnd = printHeadSetup.message(new byte[]{-86, 85, 23, 0, 0, 0, 0, 0, 0, 0, 23});
|
||||
|
||||
|
||||
PrinterCommand(int cmdId) {
|
||||
this.cmdId = cmdId;
|
||||
}
|
||||
|
||||
public static PrinterCommand fromId(int id) {
|
||||
for (final PrinterCommand printerCommand : PrinterCommand.values()) {
|
||||
if (printerCommand.cmdId == (id & 0xff)) {
|
||||
return printerCommand;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static byte[] feedPaper(final int lines) {
|
||||
return feedPaper.message(new byte[]{(byte) (lines & 0xff), (byte) ((lines >> 8) & 0xFF)});
|
||||
}
|
||||
|
||||
private static byte[] intensity(final int value) {
|
||||
return intensity.message(new byte[]{(byte) (value & 0xff), (byte) ((value >> 8) & 0xFF)});
|
||||
}
|
||||
|
||||
private static byte[] quality(final int level) {
|
||||
if (level < 2)
|
||||
return quality.message(new byte[]{49});
|
||||
if (level > 4)
|
||||
return quality.message(new byte[]{53});
|
||||
return quality.message(new byte[]{(byte) (48 + level)});
|
||||
}
|
||||
|
||||
public byte[] message(byte[] payload) {
|
||||
final ByteBuffer buf = ByteBuffer.allocate(preamble.length + 4 + payload.length + 1 + 1);
|
||||
buf.put(preamble);
|
||||
buf.put((byte) this.cmdId);
|
||||
buf.put((byte) 0x00); //0x00 phone->printer (request), 0x01 printer->phone (response)
|
||||
buf.put((byte) payload.length);
|
||||
buf.put((byte) 0x00);
|
||||
buf.put(payload);
|
||||
buf.put((byte) getCrc8(payload));
|
||||
buf.put((byte) 0xff);
|
||||
|
||||
return buf.array();
|
||||
}
|
||||
}
|
||||
|
||||
private static class EncodingUtils {
|
||||
|
||||
private static ByteBuffer encodeRowRLE(BitSet binaryData, int maxEncodedLength) throws BufferOverflowException {
|
||||
final ByteBuffer encodedData = ByteBuffer.allocate(maxEncodedLength);
|
||||
|
||||
boolean currentValue = binaryData.get(0);
|
||||
int count = 0;
|
||||
|
||||
for (int i = 0; i < binaryData.length(); i++) {
|
||||
boolean bit = binaryData.get(i);
|
||||
if (bit == currentValue) {
|
||||
count++;
|
||||
if (count == 128) {
|
||||
encodedData.put(rleCountToByte(currentValue, 127));
|
||||
count = 1;
|
||||
}
|
||||
} else {
|
||||
encodedData.put(rleCountToByte(currentValue, count));
|
||||
currentValue = bit;
|
||||
count = 1;
|
||||
}
|
||||
}
|
||||
encodedData.put(rleCountToByte(currentValue, count));
|
||||
encodedData.flip();
|
||||
|
||||
return encodedData;
|
||||
}
|
||||
|
||||
private static byte rleCountToByte(boolean value, int count) {
|
||||
return (byte) ((value ? 1 : 0) << 7 | (count & 0x7F));
|
||||
}
|
||||
|
||||
private static byte readBitsAsInt(BitSet bitSet) {
|
||||
int result = 0;
|
||||
|
||||
for (int i = 7; i >= 0; i--) {
|
||||
if (bitSet.get(i)) {
|
||||
result |= (1 << (i));
|
||||
}
|
||||
}
|
||||
|
||||
return (byte) (result & 0xff);
|
||||
}
|
||||
|
||||
private static ByteBuffer encodeRowPlain(BitSet rowBits) {
|
||||
final ByteBuffer rowContent = ByteBuffer.allocate(rowBits.size() / 8);
|
||||
for (int octetIdx = 0; octetIdx < rowBits.size() / 8; octetIdx++) {
|
||||
final int bitIdx = octetIdx * 8;
|
||||
rowContent.put(readBitsAsInt(rowBits.get(bitIdx, bitIdx + 8)));
|
||||
}
|
||||
rowContent.flip();
|
||||
return rowContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="45dp"
|
||||
android:height="45dp"
|
||||
android:viewportWidth="28.74"
|
||||
android:viewportHeight="28.74">
|
||||
|
||||
<path
|
||||
android:fillColor="#1f7fdb"
|
||||
android:pathData="M3.871,3.877L24.796,3.877A0.947,0.947 0,0 1,25.744 4.824L25.744,24.834A0.947,0.947 0,0 1,24.796 25.782L3.871,25.782A0.947,0.947 0,0 1,2.924 24.834L2.924,4.824A0.947,0.947 0,0 1,3.871 3.877z"
|
||||
android:strokeWidth="3.57115"
|
||||
android:strokeColor="#00000000" />
|
||||
|
||||
<path
|
||||
android:fillColor="#4dabf5"
|
||||
android:pathData="M3.879,3.035L24.804,3.035A0.947,0.947 0,0 1,25.751 3.982L25.751,23.992A0.947,0.947 0,0 1,24.804 24.94L3.879,24.94A0.947,0.947 0,0 1,2.932 23.992L2.932,3.982A0.947,0.947 0,0 1,3.879 3.035z"
|
||||
android:strokeWidth="3.57115"
|
||||
android:strokeColor="#00000000" />
|
||||
|
||||
<path
|
||||
android:fillColor="#2196f3"
|
||||
android:pathData="M3.871,3.413L24.796,3.413A0.947,0.947 0,0 1,25.743 4.36L25.743,24.37A0.947,0.947 0,0 1,24.796 25.318L3.871,25.318A0.947,0.947 0,0 1,2.924 24.37L2.924,4.36A0.947,0.947 0,0 1,3.871 3.413z"
|
||||
android:strokeWidth="3.57115"
|
||||
android:strokeColor="#00000000" />
|
||||
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M10.068,4.87L18.673,4.87A5.729,5.729 0,0 1,24.401 10.599L24.401,18.141A5.729,5.729 0,0 1,18.673 23.87L10.068,23.87A5.729,5.729 0,0 1,4.339 18.141L4.339,10.599A5.729,5.729 0,0 1,10.068 4.87z"
|
||||
android:strokeWidth="1.2"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M4.284,14.928H24.456"
|
||||
android:strokeWidth="0.524934"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter" />
|
||||
|
||||
<path
|
||||
android:fillAlpha="0.846154"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M14.37,19.524m-2.254,0a2.254,2.254 0,1 1,4.507 0a2.254,2.254 0,1 1,-4.507 0"
|
||||
android:strokeWidth="1.04987"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,47 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="45dp"
|
||||
android:height="45dp"
|
||||
android:viewportWidth="28.74"
|
||||
android:viewportHeight="28.74">
|
||||
|
||||
<path
|
||||
android:fillColor="#7a7a7a"
|
||||
android:pathData="M3.871 3.877h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.824a0.947 0.947 0 0 1 0.947-0.947z"
|
||||
android:strokeWidth="3.57115173" />
|
||||
<path
|
||||
android:fillAlpha="0.9411765"
|
||||
android:fillColor="#9f9f9f"
|
||||
android:pathData="M3.879 3.035h20.925a0.947 0.947 0 0 1 0.947 0.947v20.01a0.947 0.947 0 0 1-0.947 0.948H3.88a0.947 0.947 0 0 1-0.947-0.948V3.982A0.947 0.947 0 0 1 3.88 3.035z"
|
||||
android:strokeWidth="3.57115173" />
|
||||
<path
|
||||
android:fillColor="#8a8a8a"
|
||||
android:pathData="M3.871 3.413h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.36A0.947 0.947 0 0 1 3.87 3.413z"
|
||||
android:strokeWidth="3.57115173" />
|
||||
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M10.068,4.87L18.673,4.87A5.729,5.729 0,0 1,24.401 10.599L24.401,18.141A5.729,5.729 0,0 1,18.673 23.87L10.068,23.87A5.729,5.729 0,0 1,4.339 18.141L4.339,10.599A5.729,5.729 0,0 1,10.068 4.87z"
|
||||
android:strokeWidth="1.2"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M4.284,14.928H24.456"
|
||||
android:strokeWidth="0.524934"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="butt"
|
||||
android:strokeLineJoin="miter" />
|
||||
|
||||
<path
|
||||
android:fillAlpha="0.846154"
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M14.37,19.524m-2.254,0a2.254,2.254 0,1 1,4.507 0a2.254,2.254 0,1 1,-4.507 0"
|
||||
android:strokeWidth="1.04987"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/sendToPrinterButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:text="@string/activity_print__image_print_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/warning_devices" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/convertedImage"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.498"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/switchDithering" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/incomingImage"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/warning_devices"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text=""
|
||||
app:layout_constraintBottom_toTopOf="@+id/sendToPrinterButton"
|
||||
app:layout_constraintTop_toBottomOf="@+id/convertedImage"
|
||||
tools:layout_editor_absoluteX="0dp" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/switchDithering"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:text="@string/activity_print_image_apply_dithering_switch"
|
||||
app:layout_constraintBottom_toTopOf="@+id/warning_devices"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/incomingImage" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -2081,6 +2081,7 @@
|
||||
<string name="devicetype_bohemic_smart_bracelet">Bohemic Smart Bracelet</string>
|
||||
<string name="devicetype_vivitar_hr_bp_monitor_activity_tracker">Vivitar HR & BP Monitor Activity Tracker</string>
|
||||
<string name="devicetype_hama_fit6900">Hama Fit6900</string>
|
||||
<string name="devicetype_generic_thermal_printer">Thermal Printer</string>
|
||||
<!-- Menus on the smart device -->
|
||||
<string name="menuitem_nothing">Nothing</string>
|
||||
<string name="menuitem_status">Status</string>
|
||||
@@ -4102,4 +4103,6 @@
|
||||
<string name="pref_atc_ble_oepl_ble_adv_interval_title">BLE Advertising Interval</string>
|
||||
<string name="pref_atc_ble_oepl_oepl_protocol_enable_title">Enable OEPL protocol</string>
|
||||
<string name="pref_atc_ble_oepl_oepl_protocol_enable_summary">Disable this if only using BLE to save power</string>
|
||||
<string name="activity_print__image_print_button">Print</string>
|
||||
<string name="activity_print_image_apply_dithering_switch">Apply Dithering</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user