Serial communication
Table of Contents
The adacl_serial crate is a small companion for console-style serial
protocols: SwissMicros DM-series calculators, HP-IL emulators, and similar devices. It is two packages, not a full
protocol stack.
| Package | Purpose |
|---|---|
| AdaCL.Serial_Communications | Open, configure, read and write a serial port. Same contract as GNAT.Serial_Communications. |
| AdaCL.Serial_IO | Character and string I/O on that port: Get, Put, Get_Line, Put_Line, Expect. |
AdaCL.Serial_IO is the package you call from application code. It is SPARK-friendly, with contracts, and proven to
SPARK gold. The reference for each subprogram is the GNATdoc output; this page only shows how to get a
port open and a line across it.
Useful links:
- Crate:
adacl_serial(depends onadacl ^8.0) - Documentation: GNATdoc
- Source: SourceForge
Licensed under GPL-3.0-or-later.
Why a second serial package # #
GNAT.Serial_Communications
is, in GNAT’s own words, supported on GNU/Linux and Windows only. On those two systems AdaCL.Serial_Communications is
a rename:
with GNAT.Serial_Communications;
package AdaCL.Serial_Communications renames GNAT.Serial_Communications;
Application code therefore sees one name on every host. On Linux and Windows the body is still GNAT’s.
macOS is the gap. There is no supported GNAT body for Darwin, so the crate supplies its own implementation of the same
specification: Open, Set, Read, Write, Close, Name, and the To_Ada / To_C descriptor conversions. Rate,
bits, stop bits, parity and flow control use the same enumeration literals as GNAT (B9600, CS8, One, None,
RTS_CTS, …). A program written against AdaCL.Serial_Communications does not change when it is built on macOS.
A pull request against GCC would be the proper long-term home for that body. I have no experience preparing one. It would take days, if not weeks, to assemble, and then years on the GCC release train before the fix arrived in an Alire toolchain. Shipping a compatible package in this crate is the way to have a working port on macOS now, without waiting for that.
Opening a port # #
Name builds a legacy PC port name: COM<number>: on Windows, ttyS<number-1> on Linux. It does not know about USB
serial devices, nor about macOS. For those, pass the device path yourself — /dev/ttyACM0 or /dev/ttyUSB0 on Linux,
/dev/cu.usbmodem… on macOS (cu rather than tty, so open does not wait on carrier detect).
AdaCL.Serial_IO.Serial_Port is a null-record extension of the communications port. The extension is what turns the
operations into methods (Port.Put_Line, Port.Expect). Open and Set are inherited as specific-type operations, so
the call uses a view conversion to the parent.
with AdaCL.Serial_Communications;
with AdaCL.Serial_IO;
procedure Talk is
package Comm renames AdaCL.Serial_Communications;
Port : AdaCL.Serial_IO.Serial_Port;
Line : String (1 .. 80);
Last : Natural;
begin
Comm.Open (Comm.Serial_Port (Port), "/dev/cu.usbmodem1101");
Comm.Set
(Port => Comm.Serial_Port (Port),
Rate => Comm.B9600,
Bits => Comm.CS8,
Parity => Comm.None,
Block => False,
Timeout => 2.0);
Port.Put_Line ("ID?");
Port.Get_Line (Line, Last);
Comm.Close (Comm.Serial_Port (Port));
end Talk;
Set defaults to 9600 baud, 8 data bits, one stop bit, no parity, blocking reads and a 10 second timeout. Blocking
means a read waits until the whole buffer has been filled. For a calculator protocol that answers with a short line,
Block => False and a modest Timeout are the usual choice. The timeout precision is limited by the host; on GNU/Linux
it is a tenth of a second. Calling Set during a transfer can reset the port hardware and drop bytes already buffered.
Serial_Error is raised when the port cannot be opened, or when a later read or write fails.
Characters and lines # #
AdaCL.Serial_IO converts between Stream_Element and Character for you. The operations mirror Ada.Text_IO, with
two serial-specific differences: Put_Line terminates with CR+LF, and Get_Line stops on either CR or LF and does not
store the terminator.
| Operation | Effect |
|---|---|
Get |
One character. Raises Data_Error if nothing arrived. |
Get into a string |
Up to Text'Length characters. Last is Text'First - 1 if none arrived. |
Get_Line |
Up to, but not including, CR or LF. Data_Error if the line does not fit. |
Put |
One character, or a string, with no terminator added. |
Put_Line |
The string, then CR+LF. |
New_Line |
CR+LF alone. |
The function form of Get has side effects. SPARK allows only one such call in a statement, so store the character
before using it in an expression:
Key : Character;
Key := Port.Get;
Waiting for a prompt # #
Expect reads until a given sequence has arrived. The sequence itself is consumed and not stored. Anything before it is
either discarded or captured.
Port.Expect ("DM42"); -- discard up to 4_096 characters
Port.Expect ("RDY", Line, Last); -- keep what preceded "RDY"
The discarding form takes Max_Characters (default 4_096) and raises Data_Error if the sequence has not appeared by
then. The capturing form raises Data_Error if the preceding text would overflow Text. An I/O failure is still
Serial_Error, not Data_Error.
Where to go next # #
Subprogram contracts, preconditions and the conversion functions are in the GNATdoc pages for
AdaCL.Serial_Communications and
AdaCL.Serial_IO. This page will not repeat them.