unit Android.Serial;
{
//usage
// In your FMX Form unit...
uses
System.IOUtils,
Android.Serial; // <-- Add the new unit
...
procedure TMyForm.Button1Click(Sender: TObject);
var
SerialPort: TAndroidSerialPort;
DataToSend: string;
Buffer: TBytes;
BytesRead: Integer;
begin
// Create an instance for /dev/ttyS9
SerialPort := TAndroidSerialPort.Create('ttyS9');
MemoLog.Lines.Add('Attempting to communicate with ' + SerialPort.DeviceName);
try
try
// 1. Open the port
SerialPort.Open;
MemoLog.Lines.Add('Port opened successfully.');
// 2. Configure the port settings (e.g., 9600 baud, 8 data bits, No parity, 1 stop bit)
SerialPort.Configure(9600, 8, 'N', 1);
MemoLog.Lines.Add('Port configured: 9600, 8N1');
// 3. Write data to the peripheral
DataToSend := 'Hello from Delphi App!';
SerialPort.WriteString(DataToSend + #13#10); // Send string with CR+LF
MemoLog.Lines.Add('Sent: ' + DataToSend);
// 4. Wait a bit for the device to respond
// IMPORTANT: In a real app, do not sleep the main thread!
// Use a TThread or TTask for reading. This is just for a simple example.
Sleep(500);
// 5. Read data back
// The Read method will return immediately or after a short timeout (set in Configure)
BytesRead := SerialPort.Read(Buffer, 256); // Attempt to read up to 256 bytes
if BytesRead > 0 then
begin
MemoLog.Lines.Add(Format('Received %d bytes: %s', [BytesRead, TEncoding.UTF8.GetString(Buffer)]));
end
else
begin
MemoLog.Lines.Add('No data received.');
end;
except
on E: ESerialPortError do
begin
MemoLog.Lines.Add('ERROR: ' + E.Message);
end;
end;
finally
// 6. Ensure the port is closed and the object is freed
if Assigned(SerialPort) then
begin
SerialPort.Close;
SerialPort.Free;
MemoLog.Lines.Add('Port closed and resources freed.');
end;
end;
end;
}
interface
uses
System.SysUtils, System.Classes, System.SyncObjs;
type
// Exception class for serial port errors
ESerialPortError =
class(
Exception);
// A class to encapsulate low-level TTY communication on Android
TAndroidSerialPort =
class
private
FDeviceName:
string;
FFileDescriptor: Integer;
FIsOpened: Boolean;
procedure SetBaudRate(
const Value: Integer);
procedure SetDataBits(
const Value: Integer);
procedure SetParity(
const Value: Char);
// 'N', 'E', 'O'
procedure SetStopBits(
const Value: Integer);
function CheckOpened: Boolean;
public
constructor Create(
const ADeviceName:
string);
destructor Destroy;
override;
// --- Core Methods ---
procedure Open;
procedure Close;
procedure Configure(BaudRate: Integer; DataBits: Integer; Parity: Char; StopBits: Integer);
// --- Read/Write Methods ---
function Read(
var Buffer: TBytes; Count: Integer): Integer;
function Write(
const Buffer: TBytes; Count: Integer): Integer;
procedure WriteString(
const s:
string);
// --- Properties ---
property IsOpened: Boolean
read FIsOpened;
property DeviceName:
string read FDeviceName;
property FileDescriptor: Integer
read FFileDescriptor;
end;
//------------------------------------------------------------------------------
// Implementation
//------------------------------------------------------------------------------
implementation
// We need to import the low-level POSIX functions for Android.
// These units contain the definitions for open, read, write, close, tcgetattr, etc.
{$IFDEF ANDROID}
uses
Posix.Fcntl, Posix.SysStat, Posix.Termios, Posix.Unistd, Androidapi.Log;
{$ENDIF}
const
TAG = '
AndroidSerial';
// For logging in logcat
{ TAndroidSerialPort }
constructor TAndroidSerialPort.Create(
const ADeviceName:
string);
begin
inherited Create;
if not ADeviceName.StartsWith('
/dev/')
then
FDeviceName := '
/dev/' + ADeviceName
else
FDeviceName := ADeviceName;
FFileDescriptor := -1;
FIsOpened := False;
end;
destructor TAndroidSerialPort.Destroy;
begin
if FIsOpened
then
Close;
inherited Destroy;
end;
procedure TAndroidSerialPort.Open;
{$IFDEF ANDROID}
begin
if FIsOpened
then
Exit;
// O_RDWR: Open for reading and writing.
// O_NOCTTY: The port will not become the controlling terminal of the process.
// O_NDELAY: Don't wait for DCD line (data carrier detect).
FFileDescriptor := Posix.Fcntl.open(PAnsiChar(AnsiString(FDeviceName)), O_RDWR
or O_NOCTTY
or O_NDELAY);
if FFileDescriptor = -1
then
begin
FIsOpened := False;
// Log the specific error for debugging via logcat
Androidapi.Log.d(TAG, '
Failed to open serial port %s. Error: %s', [FDeviceName, strerror(errno)]);
raise ESerialPortError.CreateFmt('
Cannot open serial port %s. Check permissions or if the device exists. Error: %s', [FDeviceName, strerror(errno)]);
end
else
begin
FIsOpened := True;
// Set file status flags
fcntl(FFileDescriptor, F_SETFL, 0);
Androidapi.Log.d(TAG, '
Successfully opened port %s with FD: %d', [FDeviceName, FFileDescriptor]);
end;
end;
{$ELSE}
begin
// This is a placeholder for non-Android platforms
raise ESerialPortError.Create('
This class is only for the Android platform.');
end;
{$ENDIF}
procedure TAndroidSerialPort.Close;
{$IFDEF ANDROID}
begin
if not FIsOpened
then
Exit;
if FFileDescriptor <> -1
then
begin
Posix.Unistd.close(FFileDescriptor);
FFileDescriptor := -1;
FIsOpened := False;
Androidapi.Log.d(TAG, '
Closed port %s', [FDeviceName]);
end;
end;
{$ELSE}
begin
// Placeholder
end;
{$ENDIF}
procedure TAndroidSerialPort.Configure(BaudRate: Integer; DataBits: Integer; Parity: Char; StopBits: Integer);
{$IFDEF ANDROID}
var
options: termios;
baudConstant: Integer;
begin
if not CheckOpened
then Exit;
// 1. Get the current options for the port
if tcgetattr(FFileDescriptor, options) <> 0
then
begin
Androidapi.Log.d(TAG, '
Failed to get termios attributes. Error: %s', [strerror(errno)]);
raise ESerialPortError.Create('
Failed to get terminal attributes.');
end;
// 2. Set the baud rate (input and output)
case BaudRate
of
9600: baudConstant := B9600;
19200: baudConstant := B19200;
38400: baudConstant := B38400;
57600: baudConstant := B57600;
115200: baudConstant := B115200;
else baudConstant := B9600;
// Default
end;
cfsetispeed(options, baudConstant);
cfsetospeed(options, baudConstant);
// 3. Set control flags (c_cflag)
options.c_cflag := options.c_cflag
or CLOCAL
or CREAD;
// Enable receiver, ignore modem control lines
// 4. Set data bits
options.c_cflag := options.c_cflag
and not CSIZE;
// Clear the mask
case DataBits
of
7: options.c_cflag := options.c_cflag
or CS7;
8: options.c_cflag := options.c_cflag
or CS8;
else
options.c_cflag := options.c_cflag
or CS8;
// Default to 8
end;
// 5. Set parity
case UpCase(Parity)
of
'
N':
begin // No parity
options.c_cflag := options.c_cflag
and not PARENB;
options.c_iflag := options.c_iflag
and not INPCK;
end;
'
E':
begin // Even parity
options.c_cflag := options.c_cflag
or PARENB;
options.c_cflag := options.c_cflag
and not PARODD;
options.c_iflag := options.c_iflag
or INPCK;
end;
'
O':
begin // Odd parity
options.c_cflag := options.c_cflag
or PARENB
or PARODD;
options.c_iflag := options.c_iflag
or INPCK;
end;
end;
// 6. Set stop bits
if StopBits = 2
then
options.c_cflag := options.c_cflag
or CSTOPB
// 2 stop bits
else
options.c_cflag := options.c_cflag
and not CSTOPB;
// 1 stop bit (default)
// 7. Set local flags (c_lflag) -> RAW MODE
// This makes the terminal input and output raw.
options.c_lflag := options.c_lflag
and not (ICANON
or ECHO
or ECHOE
or ISIG);
// 8. Set output flags (c_oflag) -> RAW MODE
options.c_oflag := options.c_oflag
and not OPOST;
// 9. Set read timeout settings
options.c_cc[VMIN] := 0;
// Minimum number of characters for non-canonical read
options.c_cc[VTIME] := 10;
// Timeout in deciseconds (e.g., 10 = 1 second) for non-canonical read
// 10. Apply the new options
// TCSANOW: the change occurs immediately.
if tcsetattr(FFileDescriptor, TCSANOW, options) <> 0
then
begin
Androidapi.Log.d(TAG, '
Failed to set termios attributes. Error: %s', [strerror(errno)]);
raise ESerialPortError.Create('
Failed to set terminal attributes.');
end;
// Flush the port
tcflush(FFileDescriptor, TCIFLUSH);
Androidapi.Log.d(TAG, '
Successfully configured port %s', [FDeviceName]);
end;
{$ELSE}
begin
// Placeholder
end;
{$ENDIF}
function TAndroidSerialPort.CheckOpened: Boolean;
begin
Result := FIsOpened;
if not Result
then
raise ESerialPortError.Create('
Serial port is not open.');
end;
function TAndroidSerialPort.
Read(
var Buffer: TBytes; Count: Integer): Integer;
{$IFDEF ANDROID}
begin
if not CheckOpened
then Exit(0);
if Count <= 0
then Exit(0);
SetLength(Buffer, Count);
Result := Posix.Unistd.
read(FFileDescriptor, Buffer, Count);
if Result < 0
then
begin
// An error occurred
Androidapi.Log.d(TAG, '
Error reading from serial port. Error: %s', [strerror(errno)]);
raise ESerialPortError.CreateFmt('
Error reading from serial port: %s', [strerror(errno)]);
end;
// Resize buffer to actual number of bytes read
SetLength(Buffer, Result);
end;
{$ELSE}
begin
Result := 0;
// Placeholder
end;
{$ENDIF}
function TAndroidSerialPort.
Write(
const Buffer: TBytes; Count: Integer): Integer;
{$IFDEF ANDROID}
begin
if not CheckOpened
then Exit(0);
if Count <= 0
then Exit(0);
Result := Posix.Unistd.
write(FFileDescriptor, Buffer, Count);
if Result < 0
then
begin
Androidapi.Log.d(TAG, '
Error writing to serial port. Error: %s', [strerror(errno)]);
raise ESerialPortError.CreateFmt('
Error writing to serial port: %s', [strerror(errno)]);
end;
end;
{$ELSE}
begin
Result := 0;
// Placeholder
end;
{$ENDIF}
procedure TAndroidSerialPort.WriteString(
const s:
string);
var
bytesToWrite: TBytes;
begin
// Convert Delphi string to TBytes for writing
bytesToWrite := TEncoding.UTF8.GetBytes(s);
Write(bytesToWrite, Length(bytesToWrite));
end;
// --- These are just placeholders for the property setters for now ---
// A real implementation might re-call Configure.
procedure TAndroidSerialPort.SetBaudRate(
const Value: Integer);
begin
// Re-configure would be needed here
end;
procedure TAndroidSerialPort.SetDataBits(
const Value: Integer);
begin
// Re-configure would be needed here
end;
procedure TAndroidSerialPort.SetParity(
const Value: Char);
begin
// Re-configure would be needed here
end;
procedure TAndroidSerialPort.SetStopBits(
const Value: Integer);
begin
// Re-configure would be needed here
end;
end.