Wednesday, June 17, 2026

Android java: migrate build to gradle version catalog

create a new gradle catalog, project -> gradle -> new -> Version Catalog, it will prompt "lib" and press enter.

fill libs.version.toml with agp version


[versions]
agp = "9.2.1"
appcompat = "1.7.1"
material = "1.14.0"
constraintlayout = "2.2.1"
junit = "4.13.2"
androidx-junit = "1.3.0"
espresso-core = "3.7.0"
play-services-ads-lite = "25.0.0"

[libraries]
appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
material = { module = "com.google.android.material:material", version.ref = "material" }
constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }

junit4 = { module = "junit:junit", version.ref = "junit" }
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" }
espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso-core" }

play-services-ads-lite = { module = "com.google.android.gms:play-services-ads-lite", version.ref = "play-services-ads-lite" }

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

agp in [version] and [plugin] must exist.

edit build.gradle.kts

change


dependencies {

    implementation("androidx.appcompat:appcompat:1.7.0")
    implementation("com.google.android.material:material:1.12.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.2.1")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
    implementation("com.google.android.gms:play-services-ads-lite:23.3.0")
}

 to


dependencies {

    implementation(libs.appcompat)
    implementation(libs.material)
    implementation(libs.constraintlayout)

    implementation(libs.play.services.ads.lite)

    testImplementation(libs.junit4)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.espresso.core)
}

Synchronize project and change


plugins {
    id("com.android.application")
}

to


plugins {
    alias(libs.plugins.android.application)
} 



Wednesday, June 10, 2026

Java: reading config.conf file

example config.conf


# http and https ports configuration
http_port = 48001
https_port = 48001

# printer port
printer_port=/dev/rfcomm0
# printer size
printer_size=80mm

java code


    // configuration file config.conf
    static final String linux_file_path = "/etc/dddtescpos/config.conf";
    static final String file_path = "./config.conf";

    public static String printer_port = null;
    public static String printer_size = null;
    
    public static int http_port = -1;
    public static int https_port = -1;
    
    public static void readConfig() {
        Path configFile = Paths.get(linux_file_path);
        
        if (Files.notExists(configFile)) {
            configFile = Paths.get(file_path);
        }
        
        // comment in config file will # or !
        Properties props = new Properties();

        try (InputStream input = Files.newInputStream(configFile)) {
            props.load(input);
        
            printer_port = props.getProperty("printer_port");
            printer_size = props.getProperty("printer_size");
            if (printer_size == null || printer_size.isBlank()) {
                printer_size = "80mm";   // default
            } else {
                printer_size = printer_size.trim().toLowerCase();

                if (!printer_size.equals("58mm")
                        && !printer_size.equals("80mm")) {
                    printer_size = "80mm";   // default for invalid value
                }
            }


            String tmpPort = props.getProperty("http_port","48001");
            try {
                int intTmpPort = Integer.parseInt(tmpPort); 
                if (intTmpPort<48000 || intTmpPort>65535)
                    intTmpPort = 48001;
                    http_port = intTmpPort;
            } catch (NumberFormatException ex) {
                http_port = 48001;
            }
            tmpPort = props.getProperty("https_port","48002");
            try {
                int intTmpPort = Integer.parseInt(tmpPort); 
                if (intTmpPort<48000 || intTmpPort>65535)
                    intTmpPort = 48002;
                https_port = intTmpPort;
            } catch (NumberFormatException ex) {
                https_port = 48002;
            }
            // if ports hava equal value, set default
            if (http_port == https_port) {
                http_port = 48001;
                https_port = 48002;
            }
        } catch (IOException ex) {
            System.out.println("No Configuration file found");
            http_port = 48001;
            https_port = 48002;            
        }
        
    }

 

Monday, June 8, 2026

Java: http and https (self sign certificate)

install mkcert


# apt install mkcert

go to root of netbeans project and create self sign certificate


$ mkcert localhost
Created a new local CA 💥
Note: the local CA is not installed in the system trust store.
Note: the local CA is not installed in the Firefox and/or Chrome/Chromium trust store.
Run "mkcert -install" for certificates to be trusted automatically ⚠️

Created a new certificate valid for the following names 📜
 - "localhost"

The certificate is at "./localhost.pem" and the key at "./localhost-key.pem" ✅

It will expire on 8 September 2028 🗓

Convert to PKCS 12


$ openssl pkcs12 -export \
  -in localhost.pem \
  -inkey localhost-key.pem \
  -out localhost.p12 \
  -name localhost
Enter Export Password:
Verifying - Enter Export Password:

main java code


/*
 * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
 */

package com.dedetok.ddtescpos;

import com.alibaba.fastjson2.JSONObject;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsExchange;
import com.sun.net.httpserver.HttpsServer;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;


/**
 *
 * @author dedetok
 */
public class DdtEscPos {

    final static String serviceString = "DdtEscPos";
    final static String serviceVersion = "1.0";
    
    public static void main(String[] args) {
        System.out.println("DdtEscPos reading configuration file");
        MyController.readConfig(); // if configuration not available, use hard code config
        
        System.out.println(serviceString+" "+serviceVersion);
        
        // ================= 
        try {
            HttpServer httpServer = createHttpService(MyController.http_port);
            HttpsServer httpsServer = createHttpsService(MyController.https_port);
            
            Runtime.getRuntime().addShutdownHook(new Thread(() -> {
                System.out.println("Stopping HTTP & HTTPS server...");
                httpServer.stop(5); // stop 5 second
                System.out.println("HTTP server stopped");
                httpsServer.stop(5); // stop 5 second
                System.out.println("HTTPS server stopped");

            }));
            httpServer.start();
            System.out.println("HTTP server running on http://localhost:" + MyController.http_port);
            httpsServer.start();
            System.out.println("HTTPS server running on http://localhost:" + MyController.https_port);

        } catch (IOException | KeyStoreException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | KeyManagementException ex) {
            System.getLogger(DdtEscPos.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
        }
    }

    /*
     * create http service in httpPort
     */
    static HttpsServer createHttpsService(int httpsPort) throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException {
        // Load PKCS12 keystore
        char[] password = "escpos".toCharArray();
        KeyStore ks = KeyStore.getInstance("PKCS12");
        try (FileInputStream fis = new FileInputStream("localhost.p12")) {
            ks.load(fis, password);
        }

        KeyManagerFactory kmf = KeyManagerFactory.getInstance(
                KeyManagerFactory.getDefaultAlgorithm());
        
        kmf.init(ks, password);

        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(kmf.getKeyManagers(), null, null);

        // Create HTTPS server
        HttpsServer httpsServer =
                HttpsServer.create(
                        new InetSocketAddress("localhost", httpsPort),
                        0);        
    
        httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext));
        
        // Register endpoints
        httpsServer.createContext("/", DdtEscPos::handleHttpRequest);
        httpsServer.createContext("/printjson", DdtEscPos::handlePrintJson);        
        
        return httpsServer;
    }
    
    /*
     * create http service in httpPort
     */
    static HttpServer createHttpService(int httpPort) throws IOException {
        HttpServer httpServer =
                HttpServer.create(new InetSocketAddress("localhost", httpPort), 0);
        
        httpServer.createContext("/", DdtEscPos::handleHttpRequest);
        httpServer.createContext("/printjson", DdtEscPos::handlePrintJson);
        return httpServer;
    }

    /*
     * handling /
     */
    static void handleHttpRequest(HttpExchange exchange) throws IOException {
        // CORS
        exchange.getResponseHeaders().add(
                "Access-Control-Allow-Origin", "*");

        exchange.getResponseHeaders().add(
                "Access-Control-Allow-Headers",
                "Content-Type, Authorization");

        exchange.getResponseHeaders().add(
        "Access-Control-Allow-Methods",
        "GET, POST, OPTIONS");

        if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
            exchange.sendResponseHeaders(204, -1);
            return;
        }
        
        // UTF-8 text response
        exchange.getResponseHeaders().add(
                "Content-Type",
                "text/plain; charset=UTF-8");
        
        String response = infoService(exchange).toString();

        byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8);

        exchange.sendResponseHeaders(200, responseBytes.length);

        
        try (OutputStream os = exchange.getResponseBody()) {
            os.write(responseBytes);
        }
        System.out.println("Servicing client done"); // debug
    }

    /*
     * handling /printjson
     */
    static void handlePrintJson(HttpExchange exchange) throws IOException {
        // CORS
        exchange.getResponseHeaders().add(
                "Access-Control-Allow-Origin", "*");

        exchange.getResponseHeaders().add(
                "Access-Control-Allow-Headers",
                "Content-Type, Authorization");

        exchange.getResponseHeaders().add(
        "Access-Control-Allow-Methods",
        "GET, POST, OPTIONS");

        if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
            exchange.sendResponseHeaders(204, -1);
            return;
        }
        
        // UTF-8 text response
        exchange.getResponseHeaders().add(
                "Content-Type",
                "text/plain; charset=UTF-8");
        // get json from request
        // Read request body
        String requestBody;
        try (InputStream is = exchange.getRequestBody()) {
            requestBody = new String(is.readAllBytes(), StandardCharsets.UTF_8);
            // Parse JSON
            // Parse JSON and process
            JSONObject json = JSONObject.parseObject(requestBody);

            JSONObject responseJson = infoService(exchange);
            
            try {
                MyController.printJson(json.toString());


                responseJson.put("status", "printing done");
            } catch (IOException ex) {
                responseJson.put("status", "printing fail");
                responseJson.put("error", ex.getMessage());
                
            }
            
            byte[] bytes =
                responseJson.toString().getBytes(StandardCharsets.UTF_8);

            exchange.sendResponseHeaders(200, bytes.length);
            OutputStream os = exchange.getResponseBody();
            os.write(bytes);
            os.flush();

        }
    }
    
    /* 
     * standard info response 
     */
    static JSONObject infoService(HttpExchange exchange) {
        JSONObject jsonObj = new JSONObject();
        
        jsonObj.put("version", serviceVersion);
        jsonObj.put("service", serviceString);
        jsonObj.put("status", "running");
        String host = exchange.getLocalAddress().getHostString();
        int port = exchange.getLocalAddress().getPort();

        String scheme = (exchange instanceof HttpsExchange)
        ? "https://"
        : "http://";
        
        String baseUrl = scheme + host + ":" + port;
        jsonObj.put("endpoint",baseUrl+"/printjson");
        
        return jsonObj;
    }
}

to run service


$ java -jar target/DdtEscPos-1.0.jar 
Hello World!
HTTP server running on http://localhost:50001
HTTPS server running on https://localhost:50002
Serviing client done
Serviing client done

using curl to send json to print http


$ curl -H "Content-Type: application/json" \
     -d @exampleescpos.json \
     http://localhost:50001/printjson

https


$ curl -k \
  -H "Content-Type: application/json" \
  -d @exampleescpos.json \
  https://localhost:50002/printjson

or 


$ curl --insecure \
  -H "Content-Type: application/json" \
  -d @exampleescpos.json \
  https://localhost:50002/printjson

Credit: esc html using https://www.freeformatter.com/html-escape.html 

Saturday, June 6, 2026

Java: using thermal printer 6106 - 80mm esc/pos java in debian 13 bluetooth

Install bluetooth software


$ apt install firmware-misc-nonfree bluez bluez-tools blueman screen

linux user groups: bluetooth, lp, netdev, sudo, and plugdev

add linux user to group


# usermod -aG sudo username

check your bluetooth adapter has been recognize by debian


$ lsusb
...
Bus 001 Device 003: ID 0489:e0cd Foxconn / Hon Hai MediaTek Bluetooth Adapter
...

pair your device, if you don't hook it in boot, you need to rebind after boot. 


$ bluetoothctl pair 86:67:7A:1D:C2:E3
Attempting to pair with 86:67:7A:1D:C2:E3
[CHG] Device 86:67:7A:1D:C2:E3 Connected: yes
Failed to pair: org.bluez.Error.AuthenticationFailed

using bluetoothctl to handle bluetooth password


$ bluetoothctl
Agent registered
[bluetoothctl]> agent KeyboardOnly
Agent is already registered
[bluetoothctl]> default-agent
Default agent request successful
[bluetoothctl]> scan on
SetDiscoveryFilter success
Discovery started
[CHG] Controller BC:F4:D4:12:4C:28 Discovering: yes
[NEW] Device 4D:F9:CE:48:C4:1B 4D-F9-CE-48-C4-1B
[NEW] Device 62:FE:E1:08:CD:73 62-FE-E1-08-CD-73
[NEW] Device 6D:AC:09:7B:A7:F3 6D-AC-09-7B-A7-F3
[NEW] Device 86:67:7A:1D:C2:E3 RPP02N
[bluetoothctl]> pair 86:67:7A:1D:C2:E3
Attempting to pair with 86:67:7A:1D:C2:E3
[CHG] Device 86:67:7A:1D:C2:E3 Connected: yes
[CHG] Device 86:67:7A:1D:C2:E3 Bonded: yes
[CHG] Device 86:67:7A:1D:C2:E3 Modalias: usb:v05ACp0239d0644
[CHG] Device 86:67:7A:1D:C2:E3 ServicesResolved: yes
[CHG] Device 86:67:7A:1D:C2:E3 Paired: yes
Pairing successful
[DEL] Device 6D:AC:09:7B:A7:F3 6D-AC-09-7B-A7-F3
[CHG] Device 86:67:7A:1D:C2:E3 ServicesResolved: no
[CHG] Device 86:67:7A:1D:C2:E3 Connected: no
[DEL] Device 4D:F9:CE:48:C4:1B 4D-F9-CE-48-C4-1B
[DEL] Device 62:FE:E1:08:CD:73 62-FE-E1-08-CD-73
[bluetoothctl]> trust 86:67:7A:1D:C2:E3
[CHG] Device 86:67:7A:1D:C2:E3 Trusted: yes
Changing 86:67:7A:1D:C2:E3 trust succeeded

busctl output


$ busctl introspect org.bluez /org/bluez/hci0/dev_86_67_7A_1D_C2_E3
NAME                                TYPE      SIGNATURE RESULT/VALUE                             FLAGS
org.bluez.Device1                   interface -         -                                        -
.CancelPairing                      method    -         -                                        -
.Connect                            method    -         -                                        -
.ConnectProfile                     method    s         -                                        -
.Disconnect                         method    -         -                                        -
.DisconnectProfile                  method    s         -                                        -
.Pair                               method    -         -                                        -
.Adapter                            property  o         "/org/bluez/hci0"                        emits-change
.Address                            property  s         "86:67:7A:1D:C2:E3"                      emits-change
.AddressType                        property  s         "public"                                 emits-change
.AdvertisingData                    property  a{yv}     -                                        emits-change
.AdvertisingFlags                   property  ay        -                                        emits-change
.Alias                              property  s         "RPP02N"                                 emits-change writable
.Appearance                         property  q         -                                        emits-change
.Blocked                            property  b         false                                    emits-change writable
.Bonded                             property  b         true                                     emits-change
.Class                              property  u         263808                                   emits-change
.Connected                          property  b         false                                    emits-change
.Icon                               property  s         "printer"                                emits-change
.LegacyPairing                      property  b         false                                    emits-change
.ManufacturerData                   property  a{qv}     -                                        emits-change
.Modalias                           property  s         "usb:v05ACp0239d0644"                    emits-change
.Name                               property  s         "RPP02N"                                 emits-change
.Paired                             property  b         true                                     emits-change
.RSSI                               property  n         -                                        emits-change
.ServiceData                        property  a{sv}     -                                        emits-change
.ServicesResolved                   property  b         false                                    emits-change
.Sets                               property  a{oa{sv}} -                                        emits-change
.Trusted                            property  b         true                                     emits-change writable
.TxPower                            property  n         -                                        emits-change
.UUIDs                              property  as        7 "00000001-0000-1000-8000-00805f9b34fb… emits-change
.WakeAllowed                        property  b         -                                        emits-change writable
org.freedesktop.DBus.Introspectable interface -         -                                        -
.Introspect                         method    -         s                                        -
org.freedesktop.DBus.Properties     interface -         -                                        -
.Get                                method    ss        v                                        -
.GetAll                             method    s         a{sv}                                    -
.Set                                method    ssv       -                                        -
.PropertiesChanged                  signal    sa{sv}as  -                                        -

bind to rfcomm (sudo), if you don't hook it in boot, you need to rebind after boot. if you already trust the printer, just bind it.


  # rfcomm bind rfcomm0 86:67:7A:1D:C2:E3

or with sudo


  $ sudo  rfcomm bind rfcomm0 86:67:7A:1D:C2:E3

your dev access point


$ ls /dev/ | grep rfcomm
rfcomm0

get your bluetooth device info


$ bluetoothctl info 86:67:7A:1D:C2:E3
Device 86:67:7A:1D:C2:E3 (public)
	Name: RPP02N
	Alias: RPP02N
	Class: 0x00040680 (263808)
	Icon: printer
	Paired: yes
	Bonded: yes
	Trusted: yes
	Blocked: no
	Connected: no
	LegacyPairing: no
	UUID: SDP                       (00000001-0000-1000-8000-00805f9b34fb)
	UUID: RFCOMM                    (00000003-0000-1000-8000-00805f9b34fb)
	UUID: L2CAP                     (00000100-0000-1000-8000-00805f9b34fb)
	UUID: Serial Port               (00001101-0000-1000-8000-00805f9b34fb)
	UUID: PnP Information           (00001200-0000-1000-8000-00805f9b34fb)
	UUID: Unknown                   (000018f0-0000-1000-8000-00805f9b34fb)
	UUID: Vendor specific           (e7810a71-73ae-499d-8c15-faa9aef0c3f2)
	Modalias: usb:v05ACp0239d0644

Optional: to let systemd handling pair device

create/edit bluetooth-printer.service (or any name you wish), at /etc/systemd/system/, e.g   /etc/systemd/system/bluetooth-printer.service.


[Unit]
Description=Bind Bluetooth Thermal Printer to rfcomm0
After=bluetooth.service
Requires=bluetooth.service

[Service]
Type=oneshot
ExecStart=/usr/bin/rfcomm bind rfcomm0 86:67:7A:1D:C2:E3
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

After that use systemd to handle pairing

  • sudo systemctl daemon-reload
  • sudo systemctl enable bluetooth-printer.service
  • sudo systemctl start bluetooth-printer.service
  • systemctl status bluetooth-printer.service 

you can try previous code https://dedetoknotes.blogspot.com/2026/06/java-using-thermal-printer-6106-80mm.html by change 

devicePath = "/dev/usb/lp1" 

to

devicePath = "/dev/rfcomm0" 

Credit: https://www.freeformatter.com/html-escape.html#before-output to esc html 

Monday, June 1, 2026

Java: using thermal printer 6106 - 80mm esc/pos java in debian 13 usb

insert your printer using usb cable


# lsusb
...
Bus 008 Device 002: ID 28e9:0289 GDMicroelectronics micro-printer
Bus 009 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
# dmesg
[   70.532011] usb 8-1: new full-speed USB device number 2 using xhci_hcd
[   70.688254] usb 8-1: New USB device found, idVendor=28e9, idProduct=0289, bcdDevice= 2.00
[   70.688270] usb 8-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[   70.688276] usb 8-1: Product: micro-printer
[   70.688282] usb 8-1: Manufacturer: GEZHI
[   70.688286] usb 8-1: SerialNumber: 000000000004
[   70.730721] usblp 8-1:1.0: usblp1: USB Bidirectional printer dev 2 if 0 alt 0 proto 2 vid 0x28E9 pid 0x0289
[   70.730783] usbcore: registered new interface driver usblp
use nl80211
# ls -al /dev/usb/
total 0
drwxr-xr-x  2 root root     80 May 31 14:31 .
drwxr-xr-x 20 root root   3840 May 31 14:31 ..
crw-------  1 root root 180, 0 May 31 14:30 hiddev0
crw-rw----  1 root lp   180, 1 May 31 14:31 lp1
# ls -al /dev/usb/lp1
crw-rw---- 1 root lp 180, 1 May 31 14:31 /dev/usb/lp1

we found the device known as usb lp1 i.e. /dev/usb/lp1. test to print something.


# printf '\x1b\x40Hello World\n\n\n' > /dev/usb/lp1

your printer should print "Hello World"

add your account or linux user as lp group 


# usermod -aG lp [username]

you need to sign out and re sign in, then try to print as [username]


$ printf '\x1b\x40Hello World\n\n\n' > /dev/usb/lp1

if everything work, now you can work as [username]. this is simple netbeans 30 - maven, to test your printer.

This is pom.xml for your project.


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.dedetok</groupId>
    <artifactId>EnumComPort</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>21</maven.compiler.release>
        <exec.mainClass>com.dedetok.enumcomport.EnumComPort</exec.mainClass>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.fazecast</groupId>
            <artifactId>jSerialComm</artifactId>
            <version>2.11.0</version>
            <type>jar</type>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.5.0</version>

                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>

                        <configuration>
                            <createDependencyReducedPom>false</createDependencyReducedPom>

                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.dedetok.enumcomport.EnumComPort</mainClass>
                                </transformer>
                            </transformers>

                        </configuration>
                    </execution>
                </executions>

            </plugin>

        </plugins>
    </build>
</project>

This is the java code


package com.dedetok.enumcomport;

import com.fazecast.jSerialComm.SerialPort;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/**
 *
 * @author dedetok
 */
public class EnumComPort {

    public static void main(String[] args) throws IOException {
        System.out.println("Hello World!");
        System.out.println(System.getProperty("user.name"));
        Process p = Runtime.getRuntime().exec("id");
        p.getInputStream().transferTo(System.out);
        
        SerialPort[] ports = SerialPort.getCommPorts();

        if (ports.length == 0) {
            System.out.println("No serial ports detected.");
        }

        System.out.println("Available Serial Ports:");
        System.out.println("------------------------");

        for (SerialPort port : ports) {

            System.out.println("System Port Name : " + port.getSystemPortName());
            System.out.println("Descriptive Name: " + port.getDescriptivePortName());
            System.out.println("Port Description: " + port.getPortDescription());
            System.out.println("System Location : " + port.getSystemPortPath());
            System.out.println("------------------------");
        }
        
        String devicePath = "/dev/usb/lp1";
        // Open file in read-write mode for raw bidirectional access
        RandomAccessFile deviceFile = new RandomAccessFile(devicePath, "rw");
        FileDescriptor fd = deviceFile.getFD();
        
        try (OutputStream out = new FileOutputStream(fd)) {
            // ESC @ (initialize)
            out.write(new byte[]{0x1B, 0x40});

            out.write("Hello World\n".getBytes());

            // Feed
            out.write("\n\n".getBytes());

            // Cut
            out.write(new byte[]{0x1D, 0x56, 0x00});

            out.flush();
            out.close(); // NO NEED
        } catch (FileNotFoundException ex) {
            System.getLogger(EnumComPort.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
        }
        System.out.println("End");
    }
}

In my test, you can run inside your netbeans, Run -> Build Project or Run -> Clean and Run Project. to run the jar, you go to project root and run like this


$ java -jar ./target/EnumComPort-1.0.jar 
Hello World!
[username]
uid=1000([username]) gid=1000([username]) groups=1000([username]),7(lp),20(dialout),24(cdrom),25(floppy),29(audio),30(dip),33(www-data),44(video),46(plugdev),100(users),106(netdev),111(bluetooth),113(lpadmin),116(scanner),124(libvirt)
No serial ports detected.
Available Serial Ports:
------------------------
End

your print should print "Hello World". "No serial ports" means, you can use com.fazecast.jSerialComm library for lp printer.

Credit: https://www.freeformatter.com/html-escape.html for free html escape 

Saturday, May 30, 2026

Java: enumerate com serial using fazecast jSerialComm

Simple code to enumerate serial com port


/*
 * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
 */

package com.dedetok.enumcomport;

import com.fazecast.jSerialComm.SerialPort;

/**
 *
 * @author dedetok
 */
public class EnumComPort {

    public static void main(String[] args) {
        System.out.println("Hello World!");

        SerialPort[] ports = SerialPort.getCommPorts();

        if (ports.length == 0) {
            System.out.println("No serial ports detected.");
            return;
        }

        System.out.println("Available Serial Ports:");
        System.out.println("------------------------");

        for (SerialPort port : ports) {

            System.out.println("System Port Name : " + port.getSystemPortName());
            System.out.println("Descriptive Name: " + port.getDescriptivePortName());
            System.out.println("Port Description: " + port.getPortDescription());
            System.out.println("System Location : " + port.getSystemPortPath());
            System.out.println("------------------------");
        }
        
    }
}

Note: lpx device (e.g /dev/usb/lp1) does not use serial com, you need to send hex code direct via OutputStream.

Java: Hook shutdown to run java application as systemd

This is minimum java code to hook shutdown


/*
 * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license
 */

package com.dedetok.javaservicetest;

/**
 *
 * @author dedetok
 */
public class JavaServiceTest {

    private static volatile boolean running = true;
        
    public static void main(String[] args) {
        System.out.println("Hello World!");
        
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Shutdown hook triggered");

            running = false;

            // Cleanup resources here
            // Close database connections
            // Flush logs
            // Stop worker threads
            // Main shutdown path
            //cleanupResources();

            System.out.println("Cleanup complete");
        }));

        System.out.println("Daemon started");

        while (running) {
            System.out.println("Working...");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException ex) {
                System.getLogger(JavaServiceTest.class.getName()).log(System.Logger.Level.ERROR, (String) null, ex);
            }
        }

    }
}

to run/test this application, you can not run it inside netbeans. use terminal to run it, go to your project root and run


$ java -cp target/classes \
     com.dedetok.javaservicetest.JavaServiceTest
Hello World!
Daemon started
Working...
Working...
^CShutdown hook triggered
Cleanup complete

 

 

Java Netbeans: upgrade binary Netbeans 29 to Netbeans 30

 This step is straight forward:

  1. go to your root directory (e.g your home dir ~/), and rename old folder e.g ~/netbeans29.
  2. extract netbeans 30 binary and put it in root directory to replace old folder (e.g your home dir ~/).
  3. Run your netbeans 30, import previous configuration
  4. if no error found you safe to remove
    1. old netbeans 29 (e.g ~/netbeans29) 
    2. ~/.netbeans/29
    3. ~/.cache/netbeans/29

 

Monday, May 25, 2026

Java: maven escpos-coffee library, output to binary file

This is working demo how to create print preview and generate output.bin which is valid byte format to send to thermal printer.

Edit pom.xml and add escpos-coffee

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dedetok</groupId>
    <artifactId>myapp</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
            </plugin>

            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>3.6.3</version>
                <configuration>
                    <mainClass>com.dedetok.myapp.App</mainClass>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.dedetok.myapp.App</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

        </plugins>
    </build>

	<dependencies>

		<dependency>
			<groupId>com.github.anastaciocintra</groupId>
			<artifactId>escpos-coffee</artifactId>
			<version>4.1.0</version>
		</dependency>

	</dependencies>
</project>

run package or compile to download library

$ mvn compile

Edit App.java


package com.dedetok.myapp;

import com.github.anastaciocintra.escpos.EscPos;
import com.github.anastaciocintra.escpos.EscPosConst;
import com.github.anastaciocintra.escpos.Style;
import com.github.anastaciocintra.output.PrinterOutputStream;

import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;

public class App extends JFrame {

    private JTextArea txtPreview;
    private DecimalFormat formatter;

    public App() {

        setTitle("ESC/POS Receipt Preview");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(420, 700);
        setLocationRelativeTo(null);

        JPanel root = new JPanel(new GridBagLayout());
        root.setBackground(new Color(180, 180, 180));

        JPanel paper = new JPanel(new BorderLayout());
        paper.setPreferredSize(new Dimension(300, 600));
        paper.setBackground(Color.WHITE);

        txtPreview = new JTextArea();

        txtPreview.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 15));
        txtPreview.setEditable(false);
        txtPreview.setLineWrap(false);

        txtPreview.setBackground(Color.WHITE);
        txtPreview.setForeground(Color.BLACK);

        txtPreview.setBorder(new EmptyBorder(12, 12, 12, 12));

        JScrollPane scroll = new JScrollPane(txtPreview);
        scroll.setBorder(null);

        paper.add(scroll, BorderLayout.CENTER);

        root.add(paper);

        add(root);

        generatePreview();

        // generate ESC/POS binary file
        try {
            generateEscPosFile();
            System.out.println("ESC/POS output saved to output.bin");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void generatePreview() {

        DecimalFormatSymbols symbols = new DecimalFormatSymbols();
        symbols.setGroupingSeparator('.');

        formatter = new DecimalFormat("#,###", symbols);

        StringBuilder sb = new StringBuilder();

        sb.append(center("MY COFFEE SHOP")).append("\n");
        sb.append(center("Bluetooth ESC/POS")).append("\n");
        sb.append(center("Jl. Ubud Bali")).append("\n");

        sb.append("--------------------------------\n");

        addItem(sb, "Espresso", 2, 18000);
        addItem(sb, "Cappuccino", 1, 25000);
        addItem(sb, "Croissant", 3, 12000);

        sb.append("--------------------------------\n");

        sb.append(String.format("%-20s %10s\n",
                "SUBTOTAL",
                formatter.format(97000)));

        sb.append(String.format("%-20s %10s\n",
                "TAX",
                formatter.format(9700)));

        sb.append("--------------------------------\n");

        sb.append(String.format("%-20s %10s\n",
                "TOTAL",
                formatter.format(106700)));

        sb.append("\n");

        sb.append(center("THANK YOU")).append("\n");
        sb.append(center("PLEASE COME AGAIN")).append("\n");

        sb.append("\n\n\n");

        txtPreview.setText(sb.toString());
    }

    private void generateEscPosFile() throws IOException {

        FileOutputStream fos = new FileOutputStream("output.bin");

        EscPos escpos = new EscPos(fos);

        Style titleStyle = new Style()
                .setJustification(EscPosConst.Justification.Center)
                .setBold(true)
                .setFontSize(Style.FontSize._2, Style.FontSize._2);

        Style centerStyle = new Style()
                .setJustification(EscPosConst.Justification.Center);

        Style normalStyle = new Style();

        escpos.writeLF(titleStyle, "MY COFFEE SHOP");
        escpos.writeLF(centerStyle, "Bluetooth ESC/POS");
        escpos.writeLF(centerStyle, "Jl. Ubud Bali");

        escpos.writeLF("--------------------------------");

        addItemEscPos(escpos, "Espresso", 2, 18000);
        addItemEscPos(escpos, "Cappuccino", 1, 25000);
        addItemEscPos(escpos, "Croissant", 3, 12000);

        escpos.writeLF("--------------------------------");

        escpos.writeLF(normalStyle,
                String.format("%-20s %10s",
                        "SUBTOTAL",
                        formatter.format(97000)));

        escpos.writeLF(normalStyle,
                String.format("%-20s %10s",
                        "TAX",
                        formatter.format(9700)));

        escpos.writeLF("--------------------------------");

        escpos.writeLF(normalStyle,
                String.format("%-20s %10s",
                        "TOTAL",
                        formatter.format(106700)));

        escpos.writeLF("");

        escpos.writeLF(centerStyle, "THANK YOU");
        escpos.writeLF(centerStyle, "PLEASE COME AGAIN");

        escpos.feed(4);
        escpos.cut(EscPos.CutMode.FULL);

        escpos.close();
        fos.close();
    }

    private void addItemEscPos(EscPos escpos,
                               String item,
                               int qty,
                               int price) throws IOException {

        int total = qty * price;

        escpos.writeLF(item);

        escpos.writeLF(
                String.format(
                        " %2dx %-14s %10s",
                        qty,
                        "",
                        formatter.format(total)
                )
        );
    }

    private void addItem(StringBuilder sb,
                         String item,
                         int qty,
                         int price) {

        int total = qty * price;

        sb.append(item).append("\n");

        sb.append(String.format(
                " %2dx %-14s %10s\n",
                qty,
                "",
                formatter.format(total)
        ));
    }

    private String center(String text) {

        int width = 32;

        if (text.length() >= width)
            return text;

        int leftPadding = (width - text.length()) / 2;

        return " ".repeat(leftPadding) + text;
    }

    public static void main(String[] args) {

        SwingUtilities.invokeLater(() -> {
            new App().setVisible(true);
        });
    }
}

Note: lpx device (e.g /dev/usb/lp1) does not use serial com, you need to send hex code direct via OutputStream 

 

Sunday, May 24, 2026

Java: using maven in Debian 13

Apache Maven is an open-source build automation and project management tool primarily used for Java applications. It simplifies the development process by managing a project's build, reporting, and documentation from a single piece of information.

To install maven:

# apt install maven
Installing:                     
  maven

Installing dependencies:
...

Showing version

$ mvn --version
Apache Maven 3.9.9
Maven home: /usr/share/maven
Java version: 21.0.11, vendor: Debian, runtime: /usr/lib/jvm/java-21-openjdk-amd64
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "6.12.88+deb13-amd64", arch: "amd64", family: "unix"

Go to your workspace, and run

$ mvn archetype:generate \
  -DgroupId=com.dedetok.myapp \
  -DartifactId=myapp \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DarchetypeVersion=1.5 \
  -DinteractiveMode=false
...
$ ls myapp
pom.xml  src

DartifactId=myapp -> project root directory.

The main java code is ~/myapp/src/main/java/com/dedetok/myapp/App.java

package com.dedetok.myapp;

/**
 * Hello world!
 */
public class App {
    public static void main(String[] args) {
        System.out.println("Hello World!");
    }
}

To run the project using mvn, go to folder myapp, edit pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dedetok</groupId>
    <artifactId>myapp</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <build>
        <plugins>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
            </plugin>

            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>3.6.3</version>
                <configuration>
                    <mainClass>com.dedetok.myapp.App</mainClass>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.dedetok.myapp.App</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>

        </plugins>
    </build>

</project>

then run:

$ mvn package

Run your application 


$ mvn exec:java -Dexec.mainClass="com.dedetok.myapp.App"
[INFO] Scanning for projects...
[INFO] 
[INFO] --------------------------< com.dedetok:myap >--------------------------
[INFO] Building myap 1.0-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- exec:3.6.3:java (default-cli) @ myap ---
Hello World!
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  8.265 s
[INFO] Finished at: 2026-05-24T14:59:08+07:00
[INFO] -----------------------------------------

or

$ mvn exec:java
[INFO] Scanning for projects...
[INFO] 
[INFO] -------------------------< com.dedetok:myapp >--------------------------
[INFO] Building myapp 1.0-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- exec:3.6.3:java (default-cli) @ myapp ---
Hello World!
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  4.712 s
[INFO] Finished at: 2026-05-25T14:43:48+07:00
[INFO] ------------------------------------------------------------------------

To use old school java, copy library needed

$ mvn dependency:copy-dependencies

run using java


$ mvn compile
$ java -cp "target/classes:target/dependency/*" com.dedetok.myapp.App
Hello World!

 

 

 

Tuesday, May 19, 2026

Debian 13: configurating Syris RD200-M1-G v01.55 Card Reader Part 2, detecting card reader in Debian

Add you as group of dialout (20)


# usermod -aG dialout myuser
# usermod -aG uucp myuser
# usermod -aG lock myuser
# usermod -aG tty myuser

Note: 

  1. All of these groups may not exist on every Linux distro. (Note, this process must only be done once for each user)
  2. if you use terminal only, this will effect directly, but if you use window Manager, you require to logout and re-login to make it effect especially on Netbeans, If aftar re-login fail, try restart.  

Confirm which com the reader assign


# ls -l /dev/ttyACM0
crw-rw---- 1 root dialout 166, 0 May 19 13:43 /dev/ttyACM0

The card reader assign com at /dev/ttyACM0

This is simple java code to read card when tap on device


import com.fazecast.jSerialComm.SerialPort;
import java.io.InputStream;
import java.io.OutputStream;

/**
 *
 * @author dedetok
 */
public class DedetokSerialCom {

    static private SerialPort serialPort;
    static private int boudRate = 115200;
            
    public static void main(String[] args) {
        System.out.println("Hello World!");
        
        serialPort = SerialPort.getCommPort("/dev/ttyACM0");
        
        // Configure serial port
        serialPort.setBaudRate(boudRate);
        serialPort.setNumDataBits(8);
        serialPort.setNumStopBits(SerialPort.ONE_STOP_BIT);
        serialPort.setParity(SerialPort.NO_PARITY);
        
        // Optional timeouts
        serialPort.setComPortTimeouts(
                SerialPort.TIMEOUT_READ_SEMI_BLOCKING,
                1000,
                0
        );
        
        
        // Open port
        if (!serialPort.openPort()) {
            System.out.println("Failed to open port");
            return;
        }
        
        System.out.println("Port opened"); // debug
                
        // ------------------read mifare id card ----------------------
        System.out.println("MIFARE"); // debug
        // STX LEN CMD
        byte[] cmd = new byte[] {
                (byte)0x02, // STX
                (byte)0x01, // LEN
                (byte)0x11  // CMD Read UID
        };
        
        // Send
        serialPort.writeBytes(cmd, cmd.length);

        System.out.println("command Send"); // debug
        
        /*
        02 02 11 10 : command error
        02 02 11 01 : no card
        02 06 11 00 xx xx xx xx : mifare id xx xx xx xx
        */

        byte[] rx = new byte[64];

        int n = serialPort.readBytes(rx, rx.length);

        System.out.println("Received bytes: " + n);

                // Print raw HEX
        for (int i = 0; i < n; i++) {
            System.out.printf("%02X ", rx[i]);
        }
        System.out.println();

        // ------------------read ISO15693 inventory card ----------------------
        // NOT SUPPORTED


        System.out.println();

        serialPort.closePort();

        
        System.out.println("Port Closed"); // debug
    }
}

This is sample output with uid masked Netbeans


Hello World!
Port opened
MIFARE
command Send
Received bytes: 8
02 06 xx xx xx xx xx 52 

What are they?

02 STX
06 LENGTH
Data 1-5 xx xx xx xx xx
Checksum 52 

e.g. to read Mifare UID (0x11)

command in bytes: 02 01 11

02: run command

01: length

11: command to read Mifare UID

10 checksum ( 01 xor 11 )

if return 4 bytes: MIFARE Classic 1K/4K

if return 7 bytes: NTAG / Ultralight / DESFire

if return 8 bytes: ISO15693

Example response 4 bytes UID (classic mifare)


02 06 01 xx xx xx xx 8E 
 |  |  |  |           |
 |  |  |  |           -> checksum
 |  |  |  -------------> data uid
 |  |  ----------------> status 
 |  -------------------> length data
 ----------------------> STX

Example response 6 bytes UID


02 0A 11 00 xx xx xx xx xx xx 80 00
 |  |  |  |                    |  |
 |  |  |  |                    |  -> SAK
 |  |  |  |                    ----> checksum
 |  |  |  -------------------------> data uid
 |  |  ----------------------------> status
 |  -------------------------------> length data
 ----------------------------------> STX

 

 

 

Debian 13: configurating Syris RD200-M1-G v01.55 Card Reader Part 1, firmware upgrade

By default Syris RD200-M1-G v01.55 run in keyboard mode. It will send to text when card detected. 

You need to upgrade the firmware for support some new card and change it to com mode. At com mode, you can communication with card to utilize it.

DO THIS IN WINDOWS TO UPDATE FIRMWARE:

All documentation link provided by syris is https://syris.com/ftp/index.php. Goto SYRIS_RFID_DVD -> RD200_300 -> USB_ReaderTools
_V0291.rar (All tools and docs), download and extract. 

Inside RD200_RD300_SDK_V0193 -> UtilityTools -> FirmwareFiles, there are 2 directory v1 and v2:

  • RD200(v1)
    • RD200_M1_COM_V0271_20230901.SYB
    • RD200_M1_V0271_20230901.SYB 
  • RD200(v2)
    • RD200-M1-V2_V0290_20250603.SYB
    • RD200-M1-V2_V0291_20251029_01.SYB
    • RD200-M1-V2-COM_V0283_20240216.SYB 

Run USB_ReaderTools_V0291_20251029.exe, go to tab firmware and use RD200_M1_COM_V0271_20230901.SYB to RD200-M1-G v01.55. If you choose wrong firmware, the tools will warn you "firmware type error". upgrade it, wait until finish.

After firmware upgraded, you need to change Auto Usb to Com, e.g. com3,(RD200-M1 0155) 12110056 -> (RD200-M1 0271) 12110056

Windows part is done.

This is Debian before upgrade and change to com mode


# dmesg
[  841.193326] usb 8-1: new full-speed USB device number 2 using xhci_hcd
[  841.349102] usb 8-1: New USB device found, idVendor=0e6a, idProduct=0317, bcdDevice= 1.40
[  841.349118] usb 8-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[  841.349126] usb 8-1: Product: USB Reader
[  841.349133] usb 8-1: Manufacturer: SYRIS Technology Corp.
[  841.349139] usb 8-1: SerialNumber: 00000000
[  841.389585] input: SYRIS Technology Corp. USB Reader as /devices/pci0000:00/0000:00:08.3/0000:06:00.4/usb8/8-1/8-1:1.0/0003:0E6A:0317.0004/input/input23
[  841.505357] hid-generic 0003:0E6A:0317.0004: input,hidraw3: USB HID v1.10 Keyboard [SYRIS Technology Corp. USB Reader] on usb-0000:06:00.4-1/input0
[  841.522787] input: SYRIS Technology Corp. USB Reader Consumer Control as /devices/pci0000:00/0000:00:08.3/0000:06:00.4/usb8/8-1/8-1:1.1/0003:0E6A:0317.0005/input/input24
[  841.577455] input: SYRIS Technology Corp. USB Reader System Control as /devices/pci0000:00/0000:00:08.3/0000:06:00.4/usb8/8-1/8-1:1.1/0003:0E6A:0317.0005/input/input25
[  841.577717] hid-generic 0003:0E6A:0317.0005: input,hidraw4: USB HID v1.10 Device [SYRIS Technology Corp. USB Reader] on usb-0000:06:00.4-1/input1
# lsusb
...
Bus 008 Device 002: ID 0e6a:0317 Megawin Technology Co., Ltd USB Reader
...
# lsusb -v -d 0e6a:0317

Bus 008 Device 002: ID 0e6a:0317 Megawin Technology Co., Ltd USB Reader
Negotiated speed: Full Speed (12Mbps)
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               1.10
  bDeviceClass            0 [unknown]
  bDeviceSubClass         0 [unknown]
  bDeviceProtocol         0 
  bMaxPacketSize0         8
  idVendor           0x0e6a Megawin Technology Co., Ltd
  idProduct          0x0317 USB Reader
  bcdDevice            1.40
  iManufacturer           1 SYRIS Technology Corp.
  iProduct                2 USB Reader
  iSerial                 3 00000000
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength       0x0042
    bNumInterfaces          2
    bConfigurationValue     1
    iConfiguration          0 
    bmAttributes         0xa0
      (Bus Powered)
      Remote Wakeup
    MaxPower              100mA
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           1
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      1 Boot Interface Subclass
      bInterfaceProtocol      1 Keyboard
      iInterface              0 
        HID Device Descriptor:
          bLength                 9
          bDescriptorType        33
          bcdHID               1.10
          bCountryCode            0 Not supported
          bNumDescriptors         1
          bDescriptorType        34 (null)
          wDescriptorLength      73
          Report Descriptors: 
            ** UNAVAILABLE **
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x81  EP 1 IN
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0008  1x 8 bytes
        bInterval              10
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        1
      bAlternateSetting       0
      bNumEndpoints           2
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      0 [unknown]
      bInterfaceProtocol      0 
      iInterface              0 
        HID Device Descriptor:
          bLength                 9
          bDescriptorType        33
          bcdHID               1.10
          bCountryCode            0 Not supported
          bNumDescriptors         1
          bDescriptorType        34 (null)
          wDescriptorLength     861
          Report Descriptors: 
            ** UNAVAILABLE **
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x82  EP 2 IN
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval               1
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x03  EP 3 OUT
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval               1
Device Status:     0x0000
  (Bus Powered)

After firmware upgrade and com mode


# dmesg
[   65.250090] usb 8-1: New USB device strings: Mfr=1, Product=2, SerialNumber=0
[   65.250095] usb 8-1: Product: USB Reader
[   65.250098] usb 8-1: Manufacturer: SYRIS Technology Corp.
[   65.293079] cdc_acm 8-1:1.0: ttyACM0: USB ACM device
[   65.293108] usbcore: registered new interface driver cdc_acm
[   65.293110] cdc_acm: USB Abstract Control Model driver for USB modems and ISDN 
# lsusb
...
Bus 008 Device 002: ID 0e6a:0316 Megawin Technology Co., Ltd USB Reader
...
# lsusb -v -d 0e6a:0316
Bus 008 Device 002: ID 0e6a:0316 Megawin Technology Co., Ltd USB Reader
Negotiated speed: Full Speed (12Mbps)
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               1.10
  bDeviceClass            2 Communications
  bDeviceSubClass         0 [unknown]
  bDeviceProtocol         0 
  bMaxPacketSize0         8
  idVendor           0x0e6a Megawin Technology Co., Ltd
  idProduct          0x0316 USB Reader
  bcdDevice            1.06
  iManufacturer           1 SYRIS Technology Corp.
  iProduct                2 USB Reader
  iSerial                 0 
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength       0x0043
    bNumInterfaces          2
    bConfigurationValue     1
    iConfiguration          0 
    bmAttributes         0x80
      (Bus Powered)
    MaxPower              100mA
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           1
      bInterfaceClass         2 Communications
      bInterfaceSubClass      2 Abstract (modem)
      bInterfaceProtocol      1 AT-commands (v.25ter)
      iInterface              0 
      CDC Header:
        bcdCDC               1.10
      CDC Call Management:
        bmCapabilities       0x01
          call management
        bDataInterface          1
      CDC ACM:
        bmCapabilities       0x06
          sends break
          line coding and serial state
      CDC Union:
        bMasterInterface        0
        bSlaveInterface         1 
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x81  EP 1 IN
        bmAttributes            3
          Transfer Type            Interrupt
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval              16
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        1
      bAlternateSetting       0
      bNumEndpoints           2
      bInterfaceClass        10 CDC Data
      bInterfaceSubClass      0 [unknown]
      bInterfaceProtocol      0 
      iInterface              0 
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x82  EP 2 IN
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval               0
      Endpoint Descriptor:
        bLength                 7
        bDescriptorType         5
        bEndpointAddress     0x03  EP 3 OUT
        bmAttributes            2
          Transfer Type            Bulk
          Synch Type               None
          Usage Type               Data
        wMaxPacketSize     0x0040  1x 64 bytes
        bInterval               0
Device Status:     0x0000
  (Bus Powered)

 

 

Saturday, May 16, 2026

Android java: calculating grid column and grid row

Target emoji size 48 dp


        // ------------ EMOJI PICKER
        // target emoji size
        int finalWidth=xxx,finalHeight=yyy; // final value for IME View
        int orientationIdx = (cfg.orientation == Configuration.ORIENTATION_PORTRAIT) ? 0 : 1;
        float density = context.getResources().getDisplayMetrics().density; // 
        final float targetEmojiSize = 48; // dp
        //final float targetEmojiSize = (orientationIdx==0) ? 48f : 40f; // orientationIdx==0 -> portrait

        //final float targetEmojiWidthPx = targetEmojiSize * (mXdpi / 160f);
        //final float targetEmojiHeightPx = targetEmojiSize * (mYdpi / 160f);
        //emojiGridColumns = Math.max(1, (int) (finalWidth/targetEmojiWidthPx));
        //emojiGridRows = Math.max(1, finalHeight/targetEmojiHeightPx;
        float targetEmojiSizePx = targetEmojiSize*density;  // landscape too small
        //float targetEmojiSizePx = (orientationIdx==0) ?
        //          targetEmojiSize * (mYdpi / 160f) :
        //        targetEmojiSize * (mXdpi / 160f) ;

        emojiGridColumns = Math.max(3, (int) (finalWidth/targetEmojiSizePx));
        //emojiGridRows = Math.max(1f,
        //        ((finalHeight*0.8f)/targetEmojiSizePx)-1f); // we just use 0.8 of finalHeight
        emojiGridRows = (orientationIdx==0) ?
                Math.max(1f,
                ((finalHeight*0.8f)/targetEmojiSizePx)) :
                Math.max(1f,
                        ((finalHeight*0.8f)/targetEmojiSizePx)-1)
        ; // we just use 0.8 of finalHeight

        Log.e("dedetok", "targetEmojiSize "+targetEmojiSize+" targetEmojiSizePx "+targetEmojiSizePx); // debug
        Log.e("dedetok", "finalWidth "+finalWidth+" finalHeight "+finalHeight); // debug
        Log.e("dedetok", "emojiGridColumns "+emojiGridColumns+" emojiGridRows "+emojiGridRows); // debug

 Note, you can remove *0.8f, in this context, the emoji only occupied 0.8 of finalHeight, not full. -1 is magic forcing internal emoji picker to reduce row and recalculate the emoji size. 

Friday, May 15, 2026

Android java: calculation for ime wreda keyboard build 2026.09

Code


    // Emoji column and row
    int emojiGridColumns = 8; // store number of emoji
    float emojiGridRows = 3.5f; // store row in emoji
    // IME View size
    int finalWidth=0,finalHeight=0; // final value for IME View
    
    // -------------- set ime view size and emoji -----------------------
    // old version
    private void setIMEViewSizeAndEmojiOld() {
        // 1. Get the Calculator instance
        WindowMetricsCalculator calculator = WindowMetricsCalculator.getOrCreate();
        // 2. Compute current metrics for this specific Activity
        // This works on API 24 (Split Screen) up to API 37
        WindowMetrics metrics = calculator.computeCurrentWindowMetrics(myWredaKeyboard);

        // 3. Get the bounds (Rect)
        Rect bounds = metrics.getBounds();
        float density = myWredaKeyboard.getResources().getDisplayMetrics().density;

        int screenWidth = bounds.width();
        int screenHeight = bounds.height();
        boolean isWide = screenWidth > screenHeight;
        // Handle Navigation Bar Overlap (API 24+ Compatibility)
        // MANUAL INSET
        if (isWide) {
            // In landscape, navigation bars are usually on the left or right.
            // 48dp is the standard minimum size for system bars.
            int navBarWidth = (int) (48 * density);

            // Subtract space for both sides to be safe and centered
            screenWidth = screenWidth - (2 * navBarWidth);
        }

        float screenRatio = (float) screenWidth/screenHeight;
        float smallestWidthDp = Math.min(screenWidth, screenHeight) / density;

        int numberOfKeysColumn = 10;
        int numberOfKeysRow = 5;
        float keyWHRatioPortrait = 0.5f;// width : height = 1:2


        // --- ADAPTIVE ENGINE ---

        if (screenRatio > 0.85f && screenRatio < 1.2f) {
            // 📱 FOLDABLES / SQUARE DEVICES (Unfolded state)

            // 1. Keep it short. 25% is good because the screen is huge vertically.
            finalHeight = (int) (0.25f * screenHeight);

            // 2. Calculate the "Perfect Square" key size
            // If keys are square, width = height.
            int keyHeight = finalHeight / numberOfKeysRow;

            // 3. Set Width based on the Square Key
            // We multiply keyHeight by columns to get a keyboard that fits the keys perfectly.
            int idealWidth = keyHeight * numberOfKeysColumn;

            // 4. Center it
            // This ensures the keyboard doesn't stretch. It will sit in the middle
            // with empty space on the sides, making it reachable for thumbs.
            finalWidth = Math.min(screenWidth, idealWidth);

            // Debug for Foldable
            //Log.e("dedetok", "Foldable Mode active. Width constrained to: " + finalWidth); // debug

        } else if (isWide) {

            // 📺 LANDSCAPE (TV, Tablet, Wide Phone)
            // Goal: "Fat" Keys (Wide). You mentioned "landscape make it fat"
            // To make them fat (wider than tall), we DIVIDE by the 0.5 ratio

            if (smallestWidthDp >= 600) {
                // ️ TABLET LANDSCAPE: Keep it shorter so it doesn't cover the whole screen
                finalHeight = (int) (0.45f * screenHeight);
            } else {
                //  PHONE LANDSCAPE: Make it "bigger" to be usable
                finalHeight = (int) (0.55f * screenHeight);
            }
            // Re-calculate keyHeight AFTER setting finalHeight
            int keyHeight = finalHeight / numberOfKeysRow;
            int idealWidth = (int) (keyHeight / keyWHRatioPortrait * numberOfKeysColumn);
            //Log.e("dedetok", "screenWidth "+screenWidth+" idealWidth "+idealWidth);// debug

            // Use min to ensure it doesn't bleed off screen
            finalWidth = Math.min(screenWidth, idealWidth);

        } else {
            // phone
            if (smallestWidthDp >= 600) {
                // 🏗️ TABLET LANDSCAPE: Keep it shorter so it doesn't cover the whole screen
                finalHeight = (int) (0.30f * screenHeight);
                int keyHeight = finalHeight / numberOfKeysRow;
                int idealWidth = (int) (keyHeight / keyWHRatioPortrait * numberOfKeysColumn);

                // Use min to ensure it doesn't bleed off screen
                finalWidth = Math.min(screenWidth, idealWidth);
            } else {
                // 📱 PHONE PORTRAIT: Make it "bigger" to be usable
                finalHeight = (int) (0.35f * screenHeight);
                finalWidth = screenWidth;
            }

        }
        //Log.e("dedetok", "Width: "+finalWidth+" Height: "+finalHeight); // debug

        // ------------ now calculate emoji grid column and row
        int availableHeight = (int)(finalHeight * 0.8f);
        int emojiSize;
        if (screenRatio > 0.85f && screenRatio < 1.2f) {
            // Treat foldables like portrait tablets
            int targetRows = 4;
            int emojiSizeBox = (availableHeight / targetRows) + 6;
            emojiSize = Math.max(emojiSizeBox, 120); // slightly bigger for the big screen

            emojiGridColumns = finalWidth / emojiSize;
            emojiGridRows = (float) availableHeight / emojiSize;
        } else if (isWide && smallestWidthDp < 600) {
            // Force 9 columns to make emojis large enough to only fit ~2 rows
            emojiGridColumns = 9;
            emojiSize = finalWidth / emojiGridColumns;
            emojiGridRows = (float) availableHeight / emojiSize;
        } else {
            // Portrait: Standard 4 rows
            int targetRows = 4;
            int emojiSizeBox = (availableHeight / targetRows) + 6; // 6 space
            emojiSize = Math.max(emojiSizeBox, 100);

            emojiGridColumns = finalWidth / emojiSize;
            emojiGridRows = (float) availableHeight / emojiSize;
        }
    }    

Build 2026.09 

Friday, May 1, 2026

lamp debian 13: configure codeigniter 4


Minimum codeigniter 4.7.2: php 8.2

Target access http://localhost/app1 

Download zip codeiginiter 4 from official site and extract into folder root e.g. /home/[username]/ci4/app1.

Install required php php-mbstring

# apt install php-intl php-mbstring
# systemctl restart apache2

The following PHP extensions should be enabled on your server:

  • php-curl
  • php-json
  • php-mysqlnd/php-mysql
  • php-imagick php-gd
  • php-xml/php-simplexml

Default root folder

+-- app
+-- public
+-- tests
+-- spark
+-- system
+-- writable
|
+composer.json
+env
+LICENSE
+preload.php
+phpunit.xml.dist
+README.md

Change group and permission:


# usermod -a -G www-data [username]
# groups [username]
[username] : [username] cdrom floppy audio dip www-data video plugdev users netdev bluetooth lpadmin scanner libvirt
# chgrp www-data /home/[username] /home/[username]/ci4 /home/[username]/ci4/app1
# chmod g+x /home/[username] /home/[username]/ci4 /home/[username]/ci4/app1 

Edit /home/[username]/ci4/app1/app/Config/


<?php

namespace Config;

use CodeIgniter\Config\BaseConfig;

class App extends BaseConfig
{
    /**
     * --------------------------------------------------------------------------
     * Base Site URL
     * --------------------------------------------------------------------------
     *
     * URL to your CodeIgniter root. Typically, this will be your base URL,
     * WITH a trailing slash:
     *
     * E.g., http://example.com/
     */
    //public string $baseURL = 'http://localhost:8080/';
    public string $baseURL = 'http://localhost/app1/';


Option 1 Using subpath in user home folder

assume your root codeigniter is /home/[username]/ci4/app1

edit file /etc/apache2/sites-enabled/000-default.conf and restart


    <virtualhost>
	# The ServerName directive sets the request scheme, hostname and port that
	# the server uses to identify itself. This is used when creating
	# redirection URLs. In the context of virtual hosts, the ServerName
	# specifies what hostname must appear in the request's Host: header to
	# match this virtual host. For the default virtual host (this file) this
	# value is not decisive as it is used as a last resort host regardless.
	# However, you must set it for any further virtual host explicitly.
	#ServerName www.example.com

	ServerAdmin webmaster@localhost
	DocumentRoot /var/www/html

        # --- SUBPATH
        Alias /app1 /home/[username]/ci4/app1/public
        <directory app1="" ci4="" home="" public="" username="">
           Options Indexes FoolowSymLinks
           AllowOverride All
           Require all granted
        </directory>

~# apache2ctl configtest
AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1. Set the 'ServerName' directive globally to suppress this message
Syntax OK
~# systemctl restart apache2

No configuration in this option.

Option 2 Using subpath in copy public folder to /var/www/html/ 

this is close to shared web hosting

create folder app1 inside /var/www/html and move all content inside [ci4_project_root]/public  into app1

Edit file index.php


/*
 *---------------------------------------------------------------
 * BOOTSTRAP THE APPLICATION
 *---------------------------------------------------------------
 * This process sets up the path constants, loads and registers
 * our autoloader, along with Composer's, loads our constants
 * and fires up an environment-specific bootstrapping.
 */

// LOAD OUR PATHS CONFIG FILE
// This is the line that might need to be changed, depending on your folder structure.
//require FCPATH . '../app/Config/Paths.php'; // NOT USED
// ^^^ Change this line if you move your application folder 
// USE ABOSOLUTE PATH
require '/home/[username]/ci4/app1/app/Config/Paths.php'; 

You may do this in web shared hosting. the deference is /var/www/html and /home/[username]/public_html (usually in cpanel).

NOTE: choose one of the options above, don't mix it.

Using Sub domain e.g. app1.example.com (not tested)

Subdomain Method (Best Practice)

This is the cleaner and more secure method as it keeps the entire framework structure intact.

Steps:

  1. Upload and extract your CodeIgniter zip into the folder you specified above e.g. /home/[username]/ci4/app1/.
  2. In cPanel/Plesk, create a Subdomain (e.g., app1.domain.com).
  3. Set its Document Root for sub domain to the public folder of your project (e.g., /home/[username]/ci4/app1/public) .
  4. No code changes are needed. Your setup is complete.

For development, you can find file name env. Edit it, set CI_ENVIRONMENT = development (default is production), save it as .env (with . in leading file name).

 

Debian 13: configure php apache environment for personal development

Add user to group of www-data (this is example for CodeIgniter 4 Framework)

# usermod -a -G www-data [username]
# groups [username]
[username] : [username] cdrom floppy audio dip www-data video plugdev users netdev bluetooth lpadmin scanner libvirt
# chgrp www-data /home/[username] /home/[username]/ci4 /home/[username]/ci4/app1
# chmod g+x /home/[username] /home/[username]/ci4 /home/[username]/ci4/app1

Folder structure for code igniter api

/home/[username]/ci4/app1
|
+-- app
+-- public
+-- tests
+-- spark
+-- system
+-- writable
|
+composer.json
+env
+LICENSE
+preload.php
+phpunit.xml.dist
+README.md    

Assume apache already installed, edit file /etc/apache2/sites-enabled/000-default.conf and add virtual folder e.g localhost/app1


    <virtualhost>
	# The ServerName directive sets the request scheme, hostname and port that
	# the server uses to identify itself. This is used when creating
	# redirection URLs. In the context of virtual hosts, the ServerName
	# specifies what hostname must appear in the request's Host: header to
	# match this virtual host. For the default virtual host (this file) this
	# value is not decisive as it is used as a last resort host regardless.
	# However, you must set it for any further virtual host explicitly.
	#ServerName www.example.com

	ServerAdmin webmaster@localhost
	DocumentRoot /var/www/html

        # --- SUBPATH
        Alias /app1 /home/[username]/ci4/app1/public
        <directory app1="" ci4="" home="" public="" username="">
           Options Indexes FoolowSymLinks
           AllowOverride All
           Require all granted
        </directory>

~# apache2ctl configtest
AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1. Set the 'ServerName' directive globally to suppress this message
Syntax OK

Replace [username] with yours

Restart apache


Monday, April 6, 2026

Debian 13: trouble shooting Android Debug Bridge (adb) on kernel 6.12.74

After i upgrade the kernel from 6.12.67 to kernel 6.12.74, the kernel can not detect my phone to test android application.

Do this using command

# modprobe cdc-acm
# modprobe usb-storage
# modprobe usbserial
# usermod -aG plugdev [username]
$ groups [username]
[username] : [username] cdrom floppy audio dip video plugdev users netdev bluetooth lpadmin scanner libvirt

make sure [username] in group of plugdev


Thursday, March 12, 2026

Debian 13: installing google chrome

google chrome system requirements for debian

  • Release: Debian 10+
  • Processor: Intel Pentium 4 or later that is SSE3 capable 64-bit
  • RAM: 2GB is a minimum in some contexts, 4GB or more is recommended.
  • Storage: At least 16GB, though 32GB or more is recommended for optimal performance. 

 Download key and configure apt sources  

~# wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo tee /etc/apt/trusted.gpg.d/google.asc >/dev/null
~# ls /etc/apt/trusted.gpg.d/ | grep google
google.asc
google-chrome.gpg
~# curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | sudo gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg
~# echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list > /dev/null

Update repository

~# apt update
Hit:1 http://deb.debian.org/debian trixie-backports InRelease
Hit:2 https://deb.debian.org/debian trixie InRelease   
Hit:3 https://deb.debian.org/debian trixie-updates InRelease
Hit:4 https://dl.google.com/linux/chrome/deb stable InRelease       
Hit:5 https://security.debian.org/debian-security trixie-security InRelease
Hit:6 http://httpredir.debian.org/debian trixie InRelease
All packages are up to date
~# ls /etc/apt/sources.list.d/
google-chrome.list

Installing google chrome stable

~# apt install  google-chrome-stable 
Installing:                     
  google-chrome-stable

Summary:
  Upgrading: 0, Installing: 1, Removing: 0, Not Upgrading: 0
  Download size: 127 MB
  Space needed: 413 MB / 18.9 GB available

Get:1 https://dl.google.com/linux/chrome/deb stable/main amd64 google-chrome-stable amd64 146.0.7680.71-1 [127 MB]
Fetched 127 MB in 57s (2,254 kB/s)                                             
Selecting previously unselected package google-chrome-stable.
(Reading database ... 189434 files and directories currently installed.)
Preparing to unpack .../google-chrome-stable_146.0.7680.71-1_amd64.deb ...
Unpacking google-chrome-stable (146.0.7680.71-1) ...
Setting up google-chrome-stable (146.0.7680.71-1) ...
update-alternatives: using /usr/bin/google-chrome-stable to provide /usr/bin/x-w
ww-browser (x-www-browser) in auto mode
update-alternatives: using /usr/bin/google-chrome-stable to provide /usr/bin/gno
me-www-browser (gnome-www-browser) in auto mode
update-alternatives: using /usr/bin/google-chrome-stable to provide /usr/bin/goo
gle-chrome (google-chrome) in auto mode
Processing triggers for man-db (2.13.1-1) ...
Processing triggers for desktop-file-utils (0.28-1) ...

Done.... 

Monday, March 9, 2026

Privacy Policy for Wreda Keyboard

Privacy Policy

Wreda Keyboard

Wreda Keyboard is a simple offline keyboard application designed to provide basic typing functionality and the ability to emulate human typing. The application is free to use and does not include advertisements.

Your privacy is important to us. This Privacy Policy explains how the application handles user data.

Data Collection

Wreda Keyboard does not collect, store, or transmit personal data.

The application does not collect:

  • Names

  • Email addresses

  • Phone numbers

  • Typed text

  • Device identifiers

  • IP addresses

  • Location data

All operations performed by the application occur locally on the user’s device.

Keyboard Input

As an Input Method Editor (IME), Wreda Keyboard processes text input entered by the user in order to provide keyboard functionality.

All typed text is processed locally on the device only and is not stored, logged, transmitted, or shared with the developer or any third parties.

Clipboard Access

Wreda Keyboard may read text from the device clipboard only when the user has copied text from another application.

This clipboard content may be used by the keyboard to support the human typing emulation feature, which simulates typing behavior using text copied by the user.

Clipboard data:

  • is processed locally on the device

  • is not stored by the application

  • is not transmitted outside the device

Internet Usage

Wreda Keyboard does not require an internet connection.

The application does not communicate with external servers.

Advertising and Analytics

Wreda Keyboard does not include advertising.

The application does not use analytics services, tracking tools, or third-party SDKs.

Data Sharing

The application does not share any user data with third parties.

Data Storage

Wreda Keyboard does not store personal or typed data on the device or on any external servers.

Children’s Privacy

Wreda Keyboard does not knowingly collect personal information from children under the age of 13.

Changes to This Privacy Policy

This Privacy Policy may be updated if the application functionality changes. Any updates will be reflected in the updated version of this document.

Contact

If you have any questions about this Privacy Policy, you may contact:

dedetoke@gmail.com