Programming:  Serial communication

Serial communication is both easy and hard. Easy if we have some medium level APIs that take care of sending and receiving data (and we do have them), hard if we want to guarrantee data consistency and error free transfers. We usually use a protocol. The best way to get ourselves confused when starting to learn something is to start from the end. So for now let's throw protocols out of the window. Let's meet the APIs...

The serial port must be configured (opened), we have LibSrlPortOpen. We may want to send or receive one byte, we have LibSrlSendByte, LibSrlRecvByte, or multiple bytes LibSrlSendBlock, LibSrlRecvBlock and we close the port LibSrlPortClose.

Let's see them working. Why don't we move a rectangle over the PV screen and tell the PC its position so it can show it too? Just to be fair we also do the opposite.

PC Pocket Viewer

Now by looking at the pictures, don't think the serial cable changes rects into circles and vice versa ;) I talked about protocols and errors but this... The pictures are not simultaneous shots of the same session or you could ask me "Why didn't you rotate one of them?"

Based on this, you could make 2 players "TicTacToe" over the serial cable.

Here's PV point of view.

#include	<stdrom.h>
#include	"define.h"
#include	"libc.h"

We have a screen, some rect and an event listener.

#define SCREEN_X 0
#define SCREEN_Y 0
#define SCREEN_W 160
#define SCREEN_H 160

#define RECT_W			10
#define RECT_H			10

#define EVENT_TCH		1
#define EVENT_CRADLE	4
#define EVENT_BLD1		8

Here's our cast, two actors, one is black the other white.

#define TYPE_BLACK 0
#define TYPE_WHITE 1

int rem_x, rem_y;
int loc_x, loc_y;

How do we open a serial port? We select one IB_SRL_COM2 (9 pins one), tell it how fast we want the sequence of bits to run IB_SRL_19200BPS, no parity check IX_SRL_NONE (just like google says "i'm feeling lucky") and so on. Notice we don't allow hardware handshaking IX_SRL_NOFLOW (9 pins serial allows all 4 flow control methods). If everything's alright we can start.

bool SrlOpen()
{
	SRL_STAT srl;

	srl.port = IB_SRL_COM2;
	srl.speed = IB_SRL_19200BPS; 
	srl.parit = IX_SRL_NONE;
	srl.datab = IX_SRL_8DATA;
	srl.stopb = IX_SRL_1STOP;
	srl.fctrl = IX_SRL_NOFLOW;

	if (LibSrlPortOpen(&srl) != IW_SRL_NOERR) 
	{
		LibPutMsgDlg("LibSrlPortOpen error");
		return FALSE;
	}

	return TRUE;
}

We know we have to receive a couple of bytes each time the remote point changes the position of its piece. We test for the first coordinate (LibSrlRecvByte's not blocking), if there is nothing yet we return accordingly, if we get the byte, we know there's a second one. We get it too and return them both.

bool SrlPoll(int *x, int *y)
{
	byte data;

	if (LibSrlRecvByte(&data) == IW_SRL_NODATA)
		return FALSE;

	*x = data;

	LibSrlRecvByte(&data);

	*y = data;

	return TRUE;
}

When we make a move we have to notify the remote point, we just send it the new position. We can't fail "we're feeling lucky" ;)

bool SrlSend(int x, int y)
{
	LibSrlSendByte(IB_FOLLOW_BUSY, (byte)x);
	LibSrlSendByte(IB_FOLLOW_BUSY, (byte)y); 
	return TRUE;
}

An old friend

void PollEvents(TCHSTS far *tsts, byte event_mask)
{
	union REGS reg;
	reg.x.ax = 0x0200 | event_mask;
	reg.x.di = FP_OFF(tsts);
	reg.x.es = FP_SEG(tsts);
	int86(0x50, &reg, &reg);
}

The next functions let us control the action. See how laziness improves our skills.

bool InRect(int px, int py, int x, int y, int w, int h)
{
	return (x < px && px < x + w) && (y < py && py < y + h);
}

void DoMoveRect(int x, int y, int type)
{
	int rect_x1, rect_y1;
	int rect_x2, rect_y2;

	rect_x1 = x - RECT_W / 2;
	rect_y1 = y - RECT_H / 2;
	rect_x2 = x + RECT_W / 2;
	rect_y2 = y + RECT_H / 2;

	if (type == TYPE_BLACK)
		LibGdsReverse(rect_x1, rect_y1, rect_x2, rect_y2); 
	else
		LibGdsBox(rect_x1, rect_y1, rect_x2, rect_y2); 
}

void UpdateDisplay()
{
	LibClrDisp();
	DoMoveRect(rem_x, rem_y, TYPE_BLACK);
	DoMoveRect(loc_x, loc_y, TYPE_WHITE);
	LibPutDisp();
}

void main()
{
	TCHSTS tsts;
	bool bExit = FALSE;

	rem_x = 3 * SCREEN_W / 4;
	rem_y = 80;

	loc_x =  SCREEN_W / 4;
	loc_y = 80;

	UpdateDisplay();

	SrlOpen();

	for (;bExit == FALSE;)
	{

We poll everything. We poll the touch pad and in case of a local move we update the screen and send the move to the other end of the serial cable. If we have better things to do we "bExit". While if it's a rainy day we can always check for a couple of bytes from the serial line, once we have them we update the screen.

		PollEvents(&tsts, EVENT_TCH);

		if (tsts.y != -1 || tsts.x != -1)
		{
			if (InRect(tsts.x, tsts.y, 
					SCREEN_X + RECT_W / 2, 
					SCREEN_Y + RECT_H / 2, 
					SCREEN_W - RECT_W, 
					SCREEN_H - RECT_H))
			{
				loc_x = tsts.x;
				loc_y = tsts.y;
				UpdateDisplay();
				SrlSend(loc_x, loc_y);
			}

			if (tsts.y >= SCREEN_H) bExit = TRUE;
		}

		if (SrlPoll(&rem_x, &rem_y)) UpdateDisplay();
	}

Ok it's not a rainy day. So we go out, but... don't forget to close the door (LibSrlPortClose) and clear the buffers (LibSrlTxBufClr, LibSrlRxBufClr).

	LibSrlRxBufClr();
	LibSrlTxBufClr();
	LibSrlPortClose();
	LibJumpMenu();
}

"Hey wait a moment!" i hear you say. "How do we test it? We need the second half, the PC point of view".

Here it comes! This is the same program seen through the eyes of Visual Basic. I won't comment the code. I just want you notice that 1) the communication parameters must match (see OpenComm). 2) Visual basic is event driven, we don't do the polling, we get notified, so everytime we receive some data from the remote point, "MSComm1_OnComm" is fired and we read two bytes, this is possible because of this setting "MSComm1.RThreshold = 2" which tells the system to wait for 2 bytes before triggering. 3) Sending data is as easy as throwing a protocol out of the window "MSComm1.Output = Chr(...)"

Create an empty VB6 project. Just put a PictureBox and a MSComm control (Project\Components menu) and copy the code.

Show Code
Dim point_rem(1) As Byte
Dim point_loc(1) As Byte

Private Sub OpenComm()
    MSComm1.CommPort = 2
    MSComm1.Settings = "19200,N,8,1"
    MSComm1.RThreshold = 2
    MSComm1.InputMode = comInputModeBinary
    MSComm1.OutBufferSize = 100
    MSComm1.Handshaking = comNone
    MSComm1.PortOpen = True
End Sub

Private Sub CloseComm()
    MSComm1.PortOpen = False
End Sub

Private Sub Command1_Click()
    MSComm1.Output = Chr(CByte(Text1))
    MSComm1.Output = Chr(CByte(Text2))
End Sub

Private Sub Form_Load()
    Picture1.AutoRedraw = True
    Picture1.ScaleMode = 3
    Picture1.ScaleWidth = 160
    Picture1.ScaleHeight = 160
    
    point_rem(0) = 160 / 4
    point_rem(1) = 80
    point_loc(0) = 3 * 160 / 4
    point_loc(1) = 80
    
    DoAction
    OpenComm
    MSComm1.Output = Chr(point_loc(0))
    MSComm1.Output = Chr(point_loc(1))
End Sub

Private Sub Form_Resize()
    Picture1.Width = Form1.ScaleWidth
    Picture1.Height = Form1.ScaleHeight
    
    Picture1.ScaleMode = 3
    Picture1.ScaleWidth = 160
    Picture1.ScaleHeight = 160
    DoAction
End Sub

Private Sub Form_Unload(Cancel As Integer)
    CloseComm
End Sub

Private Sub MSComm1_OnComm()
    If MSComm1.CommEvent = 2 Then
        X = MSComm1.Input
        point_rem(0) = X(0)
        point_rem(1) = X(1)
        DoAction
    End If
End Sub

Private Sub Picture1_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single)
    If Button <> vbLeftButton Then Exit Sub
    If (X - 5 >= 0) And (X + 5 < 160) And (Y - 5 >= 0) And (Y + 5 < 160) Then
        point_loc(0) = X
        point_loc(1) = Y
        DoAction
        MSComm1.Output = Chr(CByte(X))
        MSComm1.Output = Chr(CByte(Y))
    End If
End Sub

Private Sub DoAction()
    Caption = point_rem(0) & " " & point_rem(1)
    Picture1.Cls
    
    Picture1.FillStyle = vbSolid
    Picture1.FillColor = vbBlack
    Picture1.Circle (point_loc(0), point_loc(1)), 5
    
    Picture1.FillStyle = 1
    Picture1.Circle (point_rem(0), point_rem(1)), 5
End Sub