↓Skip to main content

Serial communication

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:

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.