WinComm/delphi/KPCOMM.PAS
Scott Duensing acf1a6b691 Remove BeginUpdate/EndUpdate, fix rendering starvation, add variable docs
Remove BeginUpdate/EndUpdate batching from TKPAnsi entirely -- Write now
renders immediately via FlipToScreen after every ParseData call.  Remove
FPendingScroll (caused rendering deadlock: EndUpdate refused to call
FlipToScreen while FPendingScroll > 0, but only FlipToScreen cleared it).
DoScrollUp simplified to set FAllDirty directly.

CommEvent drain loop retained (required by edge-triggered CN_RECEIVE) but
each chunk renders immediately -- no deferred batching.  Edge-triggered
notifications verified starvation-free at all levels: ISR, driver, KPCOMM
dispatch, terminal rendering, and keyboard output path.

Add comprehensive variable comments to all project files: TKPAnsi (44
fields), TKPComm (23 fields), TMainForm (9 fields), PortStateT, and
driver globals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 18:34:19 -06:00

756 lines
21 KiB
ObjectPascal

unit KPComm;
{ KPComm - Native Delphi 1.0 serial communications component. }
{ }
{ TKPComm is a non-visual TComponent descendant providing RS-232 serial }
{ I/O via the Windows 3.1 comm API. Installs to the "KP" palette tab. }
{ }
{ Port lifecycle: OpenComm -> BuildCommDCB + SetCommState -> }
{ EnableCommNotification -> CloseComm. }
{ }
{ WM_COMMNOTIFY messages are received through a hidden utility window }
{ with a registered class and dispatched to ProcessReceiveNotify, }
{ ProcessTransmitNotify, and ProcessEventNotify. }
{ }
{ Modem line status (CTS/DSR/CD) is tracked via shadow booleans toggled }
{ on transition events from GetCommEventMask, since the 16-bit comm API }
{ only provides transition events, not absolute line levels. }
interface
uses
SysUtils, Classes, WinTypes, WinProcs, Messages;
const
{ Communication events }
comEvReceive = 1;
comEvSend = 2;
comEvCTS = 3;
comEvDSR = 4;
comEvCD = 5;
comEvRing = 6;
comEvEOF = 7;
{ Error events }
comEvtBreak = 1001;
comEvtFrame = 1004;
comEvtOverrun = 1006;
comEvtRxOver = 1008;
comEvtParity = 1009;
comEvtTxFull = 1010;
type
THandshaking = (hsNone, hsXonXoff, hsRtsCts, hsBoth);
TInputMode = (imText, imBinary);
TKPComm = class(TComponent)
private
{ Port state }
FCommId: Integer; { Comm port handle from OpenComm (-1 = closed) }
FHWndNotify: HWnd; { Hidden window receiving WM_COMMNOTIFY }
{ Configuration (published properties) }
FCommPort: Integer; { Port number (1-based: 1=COM1, 2=COM2, ...) }
FSettings: string; { Baud/parity/data/stop string (e.g. '9600,N,8,1') }
FPortOpen: Boolean; { True while port is open and operational }
FInBufferSize: Integer; { Receive ring buffer size in bytes }
FOutBufferSize: Integer; { Transmit ring buffer size in bytes }
FRThreshold: Integer; { RX byte count triggering CN_RECEIVE (0=disabled) }
FSThreshold: Integer; { TX free space triggering CN_TRANSMIT (0=disabled) }
FHandshaking: THandshaking; { Flow control mode (none/XonXoff/RtsCts/both) }
FInputLen: Integer; { Max bytes per Input read (0=up to 255) }
FInputMode: TInputMode; { Text or binary read mode }
{ Modem control lines }
FDTREnable: Boolean; { DTR line state (True=asserted) }
FRTSEnable: Boolean; { RTS line state (True=asserted) }
{ DCB options }
FNullDiscard: Boolean; { Strip null bytes from received data }
FEOFEnable: Boolean; { Detect EOF character in stream }
FParityReplace: string; { Replacement char for parity errors ('' = none) }
{ Runtime state }
FBreakState: Boolean; { True while break signal is being sent }
FCommEvent: Integer; { Last event code passed to OnComm (comEv* const) }
{ Modem line shadow state (toggled on transition events from driver) }
FCTSState: Boolean; { CTS line level (toggled on ev_CTS) }
FDSRState: Boolean; { DSR line level (toggled on ev_DSR) }
FCDState: Boolean; { DCD/RLSD line level (toggled on ev_RLSD) }
{ Event handler }
FOnComm: TNotifyEvent; { Fired on comm events; check CommEvent for code }
procedure ApplyHandshaking;
procedure ApplyOptions;
procedure ClosePort;
procedure DoCommEvent(EventCode: Integer);
function GetInBufferCount: Integer;
function GetInput: string;
function GetOutBufferCount: Integer;
procedure OpenPort;
procedure ProcessEventNotify;
procedure ProcessReceiveNotify;
procedure ProcessTransmitNotify;
procedure SetBreak(Value: Boolean);
procedure SetCommPort(Value: Integer);
procedure SetDTREnable(Value: Boolean);
procedure SetHandshaking(Value: THandshaking);
procedure SetInBufferSize(Value: Integer);
procedure SetNullDiscard(Value: Boolean);
procedure SetOutBufferSize(Value: Integer);
procedure SetOutput(const Value: string);
procedure SetParityReplace(const Value: string);
procedure SetPortOpen(Value: Boolean);
procedure SetRTSEnable(Value: Boolean);
procedure SetSettings(const Value: string);
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
property Input: string read GetInput;
property Output: string write SetOutput;
property InBufferCount: Integer read GetInBufferCount;
property OutBufferCount: Integer read GetOutBufferCount;
property CDHolding: Boolean read FCDState;
property CTSHolding: Boolean read FCTSState;
property DSRHolding: Boolean read FDSRState;
property Break: Boolean read FBreakState write SetBreak;
property CommEvent: Integer read FCommEvent;
published
property CommPort: Integer read FCommPort write SetCommPort default 1;
property Settings: string read FSettings write SetSettings;
property PortOpen: Boolean read FPortOpen write SetPortOpen default False;
property InBufferSize: Integer read FInBufferSize write SetInBufferSize default 4096;
property OutBufferSize: Integer read FOutBufferSize write SetOutBufferSize default 4096;
property RThreshold: Integer read FRThreshold write FRThreshold default 0;
property SThreshold: Integer read FSThreshold write FSThreshold default 0;
property Handshaking: THandshaking read FHandshaking write SetHandshaking default hsNone;
property InputLen: Integer read FInputLen write FInputLen default 0;
property InputMode: TInputMode read FInputMode write FInputMode default imText;
property DTREnable: Boolean read FDTREnable write SetDTREnable default True;
property RTSEnable: Boolean read FRTSEnable write SetRTSEnable default True;
property NullDiscard: Boolean read FNullDiscard write SetNullDiscard default False;
property EOFEnable: Boolean read FEOFEnable write FEOFEnable default False;
property ParityReplace: string read FParityReplace write SetParityReplace;
property OnComm: TNotifyEvent read FOnComm write FOnComm;
end;
procedure Register;
implementation
const
{ WM_COMMNOTIFY notification codes }
CN_RECEIVE = $0001;
CN_TRANSMIT = $0002;
CN_EVENT = $0004;
{ DCB Flags field bit masks. The Windows 3.1 DCB packs thirteen 1-bit }
{ fields into a single UINT (Word) at offset 12 of the structure. }
{ Delphi 1.0 maps this UINT to TDCB.Flags. }
dcbBinary = $0001;
dcbRtsDisable = $0002;
dcbParity = $0004;
dcbOutxCtsFlow = $0008;
dcbOutxDsrFlow = $0010;
dcbDtrDisable = $0020;
dcbOutX = $0040;
dcbInX = $0080;
dcbPeChar = $0100;
dcbNull = $0200;
dcbChEvt = $0400;
dcbDtrflow = $0800;
dcbRtsflow = $1000;
{ Hidden notification window class name }
NotifyClassName = 'KPCommNotify';
var
NotifyClassRegistered: Boolean; { True after RegisterClass for notification window }
{ ----------------------------------------------------------------------- }
{ Hidden notification window procedure }
{ }
{ Receives WM_COMMNOTIFY from the comm driver. Retrieves the TKPComm }
{ instance pointer stored in the window's extra bytes (offset 0) and }
{ dispatches to the appropriate Process* method. }
{ ----------------------------------------------------------------------- }
function NotifyWndProc(Wnd: HWnd; Msg: Word;
WParam: Word; LParam: Longint): Longint; export;
var
Comm: TKPComm;
NotifyCode: Word;
begin
if Msg = wm_CommNotify then
begin
Comm := TKPComm(GetWindowLong(Wnd, 0));
if (Comm <> nil) and Comm.FPortOpen and (Comm.FCommId >= 0) then
begin
NotifyCode := Word(LParam);
if (NotifyCode and CN_RECEIVE) <> 0 then
Comm.ProcessReceiveNotify;
if (NotifyCode and CN_TRANSMIT) <> 0 then
Comm.ProcessTransmitNotify;
if (NotifyCode and CN_EVENT) <> 0 then
Comm.ProcessEventNotify;
end;
Result := 0;
end
else
Result := DefWindowProc(Wnd, Msg, WParam, LParam);
end;
{ ----------------------------------------------------------------------- }
{ TKPComm }
{ ----------------------------------------------------------------------- }
procedure TKPComm.ApplyHandshaking;
var
DCB: TDCB;
begin
if GetCommState(FCommId, DCB) <> 0 then
Exit;
{ Clear all flow control flags }
DCB.Flags := DCB.Flags and
not (dcbOutxCtsFlow or dcbOutxDsrFlow or dcbOutX or dcbInX or
dcbRtsDisable or dcbRtsflow);
case FHandshaking of
hsXonXoff:
begin
DCB.Flags := DCB.Flags or dcbOutX or dcbInX;
DCB.XonChar := #$11;
DCB.XoffChar := #$13;
DCB.XonLim := 256;
DCB.XoffLim := 256;
end;
hsRtsCts:
begin
DCB.Flags := DCB.Flags or dcbOutxCtsFlow or dcbRtsflow;
end;
hsBoth:
begin
DCB.Flags := DCB.Flags or dcbOutxCtsFlow or dcbRtsflow or
dcbOutX or dcbInX;
DCB.XonChar := #$11;
DCB.XoffChar := #$13;
DCB.XonLim := 256;
DCB.XoffLim := 256;
end;
end;
DCB.Id := FCommId;
SetCommState(DCB);
end;
procedure TKPComm.ApplyOptions;
var
DCB: TDCB;
begin
if GetCommState(FCommId, DCB) <> 0 then
Exit;
if FNullDiscard then
DCB.Flags := DCB.Flags or dcbNull
else
DCB.Flags := DCB.Flags and not dcbNull;
if Length(FParityReplace) > 0 then
begin
DCB.Flags := DCB.Flags or dcbParity;
DCB.PeChar := FParityReplace[1];
end
else
begin
DCB.Flags := DCB.Flags and not dcbParity;
DCB.PeChar := #0;
end;
DCB.Id := FCommId;
SetCommState(DCB);
end;
procedure TKPComm.ClosePort;
var
Msg: TMsg;
begin
{ Set FPortOpen first to prevent stale WM_COMMNOTIFY processing }
FPortOpen := False;
if FCommId >= 0 then
begin
{ Disable notifications BEFORE dropping modem lines so the ISR }
{ stops posting WM_COMMNOTIFY while we tear down. }
EnableCommNotification(FCommId, 0, -1, -1);
if FBreakState then
begin
ClearCommBreak(FCommId);
FBreakState := False;
end;
EscapeCommFunction(FCommId, CLRDTR);
EscapeCommFunction(FCommId, CLRRTS);
CloseComm(FCommId);
FCommId := -1;
end;
if FHWndNotify <> 0 then
begin
{ Flush any stale WM_COMMNOTIFY that the ISR posted before we }
{ disabled notifications. Without this, DispatchMessage would }
{ dereference the freed window structure and lock up. }
while PeekMessage(Msg, FHWndNotify, wm_CommNotify,
wm_CommNotify, pm_Remove) do
{ discard };
DestroyWindow(FHWndNotify);
FHWndNotify := 0;
end;
FCTSState := False;
FDSRState := False;
FCDState := False;
end;
constructor TKPComm.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FCommId := -1;
FHWndNotify := 0;
FCommPort := 1;
FSettings := '9600,N,8,1';
FPortOpen := False;
FInBufferSize := 4096;
FOutBufferSize := 4096;
FRThreshold := 0;
FSThreshold := 0;
FHandshaking := hsNone;
FInputLen := 0;
FInputMode := imText;
FDTREnable := True;
FRTSEnable := True;
FNullDiscard := False;
FEOFEnable := False;
FParityReplace := '?';
FBreakState := False;
FCommEvent := 0;
FCTSState := False;
FDSRState := False;
FCDState := False;
end;
destructor TKPComm.Destroy;
begin
if FPortOpen then
ClosePort;
inherited Destroy;
end;
procedure TKPComm.DoCommEvent(EventCode: Integer);
begin
FCommEvent := EventCode;
if Assigned(FOnComm) then
FOnComm(Self);
end;
function TKPComm.GetInBufferCount: Integer;
var
Stat: TComStat;
begin
Result := 0;
if not FPortOpen or (FCommId < 0) then
Exit;
GetCommError(FCommId, Stat);
Result := Stat.cbInQue;
end;
function TKPComm.GetInput: string;
var
BytesToRead: Integer;
BytesRead: Integer;
Buf: array[0..255] of Char;
begin
Result := '';
if not FPortOpen or (FCommId < 0) then
Exit;
{ Read directly without querying GetCommError first. ReadComm }
{ returns the number of bytes actually available (up to BytesToRead) }
{ so the extra GetCommError round-trip is unnecessary overhead. }
BytesToRead := 255;
if (FInputLen > 0) and (BytesToRead > FInputLen) then
BytesToRead := FInputLen;
BytesRead := ReadComm(FCommId, @Buf, BytesToRead);
if BytesRead <= 0 then
Exit;
{ Set string length and copy data directly -- preserves embedded nulls }
{ for binary mode, and works correctly for text mode as well. }
Result[0] := Chr(BytesRead);
Move(Buf, Result[1], BytesRead);
end;
function TKPComm.GetOutBufferCount: Integer;
var
Stat: TComStat;
begin
Result := 0;
if not FPortOpen or (FCommId < 0) then
Exit;
GetCommError(FCommId, Stat);
Result := Stat.cbOutQue;
end;
procedure TKPComm.OpenPort;
var
WC: TWndClass;
DCB: TDCB;
Buf: array[0..255] of Char;
Setting: string;
RxTh: Integer;
TxTh: Integer;
begin
{ Open the comm port }
StrPCopy(Buf, 'COM' + IntToStr(FCommPort));
FCommId := OpenComm(Buf, FInBufferSize, FOutBufferSize);
if FCommId < 0 then
raise Exception.Create('Cannot open COM' + IntToStr(FCommPort) +
' (error ' + IntToStr(FCommId) + ')');
{ Configure baud/parity/data/stop from Settings string }
if GetCommState(FCommId, DCB) <> 0 then
begin
CloseComm(FCommId);
FCommId := -1;
raise Exception.Create('GetCommState failed');
end;
Setting := 'COM' + IntToStr(FCommPort) + ':' + FSettings;
StrPCopy(Buf, Setting);
if BuildCommDCB(Buf, DCB) <> 0 then
begin
CloseComm(FCommId);
FCommId := -1;
raise Exception.Create('Invalid Settings: ' + FSettings);
end;
DCB.Flags := DCB.Flags or dcbBinary;
DCB.Id := FCommId;
if SetCommState(DCB) <> 0 then
begin
CloseComm(FCommId);
FCommId := -1;
raise Exception.Create('SetCommState failed');
end;
{ Apply handshaking }
ApplyHandshaking;
{ Apply null-discard and parity-replace }
ApplyOptions;
{ Set DTR and RTS lines }
if FDTREnable then
EscapeCommFunction(FCommId, SETDTR)
else
EscapeCommFunction(FCommId, CLRDTR);
if FRTSEnable then
EscapeCommFunction(FCommId, SETRTS)
else
EscapeCommFunction(FCommId, CLRRTS);
{ Register notification window class (once per process) }
if not NotifyClassRegistered then
begin
FillChar(WC, SizeOf(WC), 0);
WC.lpfnWndProc := @NotifyWndProc;
WC.cbWndExtra := SizeOf(Longint);
WC.hInstance := HInstance;
WC.lpszClassName := NotifyClassName;
if not RegisterClass(WC) then
begin
CloseComm(FCommId);
FCommId := -1;
raise Exception.Create('RegisterClass failed');
end;
NotifyClassRegistered := True;
end;
{ Create hidden notification window and store Self for dispatch }
FHWndNotify := CreateWindow(NotifyClassName, '', ws_Popup,
0, 0, 0, 0, 0, 0, HInstance, nil);
if FHWndNotify = 0 then
begin
CloseComm(FCommId);
FCommId := -1;
raise Exception.Create('CreateWindow failed');
end;
SetWindowLong(FHWndNotify, 0, Longint(Self));
{ Enable event mask for modem status, errors, and breaks. }
{ ev_RxChar is deliberately excluded: it fires per received byte, }
{ flooding WM_COMMNOTIFY with CN_EVENT on every ISR. Data arrival }
{ is already handled by CN_RECEIVE via EnableCommNotification. }
SetCommEventMask(FCommId,
ev_CTS or ev_DSR or ev_RLSD or ev_Ring or
ev_Err or ev_Break);
{ Enable comm notifications -- -1 disables the notification }
if FRThreshold > 0 then
RxTh := FRThreshold
else
RxTh := -1;
if FSThreshold > 0 then
TxTh := FSThreshold
else
TxTh := -1;
EnableCommNotification(FCommId, FHWndNotify, RxTh, TxTh);
{ Reset modem line shadow state }
FCTSState := False;
FDSRState := False;
FCDState := False;
FPortOpen := True;
end;
procedure TKPComm.ProcessEventNotify;
var
EvtMask: Word;
Stat: TComStat;
ErrFlags: Integer;
begin
EvtMask := GetCommEventMask(FCommId,
ev_CTS or ev_DSR or ev_RLSD or ev_Ring or ev_Err or ev_Break);
if (EvtMask and ev_Break) <> 0 then
DoCommEvent(comEvtBreak);
{ Modem line shadow state: toggle on each transition event. }
{ The 16-bit comm API only provides transition events, not }
{ absolute line levels. }
if (EvtMask and ev_CTS) <> 0 then
begin
FCTSState := not FCTSState;
DoCommEvent(comEvCTS);
end;
if (EvtMask and ev_DSR) <> 0 then
begin
FDSRState := not FDSRState;
DoCommEvent(comEvDSR);
end;
if (EvtMask and ev_RLSD) <> 0 then
begin
FCDState := not FCDState;
DoCommEvent(comEvCD);
end;
if (EvtMask and ev_Ring) <> 0 then
DoCommEvent(comEvRing);
if (EvtMask and ev_Err) <> 0 then
begin
ErrFlags := GetCommError(FCommId, Stat);
if (ErrFlags and ce_Frame) <> 0 then
DoCommEvent(comEvtFrame);
if (ErrFlags and ce_Overrun) <> 0 then
DoCommEvent(comEvtOverrun);
if (ErrFlags and ce_RxOver) <> 0 then
DoCommEvent(comEvtRxOver);
if (ErrFlags and ce_RxParity) <> 0 then
DoCommEvent(comEvtParity);
if (ErrFlags and ce_TxFull) <> 0 then
DoCommEvent(comEvtTxFull);
end;
end;
procedure TKPComm.ProcessReceiveNotify;
begin
if FRThreshold <= 0 then
Exit;
{ WM_COMMNOTIFY with CN_RECEIVE means data is available -- the driver }
{ already checked the threshold. No need to call GetCommError here. }
DoCommEvent(comEvReceive);
end;
procedure TKPComm.ProcessTransmitNotify;
var
Stat: TComStat;
begin
if FSThreshold <= 0 then
Exit;
GetCommError(FCommId, Stat);
if Integer(Stat.cbOutQue) <= FSThreshold then
DoCommEvent(comEvSend);
end;
procedure TKPComm.SetBreak(Value: Boolean);
begin
FBreakState := Value;
if FPortOpen and (FCommId >= 0) then
begin
if FBreakState then
SetCommBreak(FCommId)
else
ClearCommBreak(FCommId);
end;
end;
procedure TKPComm.SetCommPort(Value: Integer);
begin
if FPortOpen then
raise Exception.Create('Cannot change CommPort while port is open');
if (Value < 1) or (Value > 16) then
raise Exception.Create('CommPort must be 1..16');
FCommPort := Value;
end;
procedure TKPComm.SetDTREnable(Value: Boolean);
begin
FDTREnable := Value;
if FPortOpen and (FCommId >= 0) then
begin
if FDTREnable then
EscapeCommFunction(FCommId, SETDTR)
else
EscapeCommFunction(FCommId, CLRDTR);
end;
end;
procedure TKPComm.SetHandshaking(Value: THandshaking);
begin
FHandshaking := Value;
if FPortOpen and (FCommId >= 0) then
ApplyHandshaking;
end;
procedure TKPComm.SetInBufferSize(Value: Integer);
begin
if FPortOpen then
raise Exception.Create('Cannot change InBufferSize while port is open');
if Value < 64 then
raise Exception.Create('InBufferSize must be >= 64');
FInBufferSize := Value;
end;
procedure TKPComm.SetNullDiscard(Value: Boolean);
begin
FNullDiscard := Value;
if FPortOpen and (FCommId >= 0) then
ApplyOptions;
end;
procedure TKPComm.SetOutBufferSize(Value: Integer);
begin
if FPortOpen then
raise Exception.Create('Cannot change OutBufferSize while port is open');
if Value < 64 then
raise Exception.Create('OutBufferSize must be >= 64');
FOutBufferSize := Value;
end;
procedure TKPComm.SetOutput(const Value: string);
var
Written: Integer;
begin
if not FPortOpen or (FCommId < 0) then
raise Exception.Create('Port is not open');
if Length(Value) > 0 then
begin
Written := WriteComm(FCommId, @Value[1], Length(Value));
if Written < 0 then
DoCommEvent(comEvtTxFull);
end;
end;
procedure TKPComm.SetParityReplace(const Value: string);
begin
FParityReplace := Value;
if FPortOpen and (FCommId >= 0) then
ApplyOptions;
end;
procedure TKPComm.SetPortOpen(Value: Boolean);
begin
if Value = FPortOpen then
Exit;
if Value then
OpenPort
else
ClosePort;
end;
procedure TKPComm.SetRTSEnable(Value: Boolean);
begin
FRTSEnable := Value;
if FPortOpen and (FCommId >= 0) then
begin
if FRTSEnable then
EscapeCommFunction(FCommId, SETRTS)
else
EscapeCommFunction(FCommId, CLRRTS);
end;
end;
procedure TKPComm.SetSettings(const Value: string);
var
DCB: TDCB;
Buf: array[0..255] of Char;
Setting: string;
begin
FSettings := Value;
if FPortOpen and (FCommId >= 0) then
begin
if GetCommState(FCommId, DCB) <> 0 then
Exit;
Setting := 'COM' + IntToStr(FCommPort) + ':' + FSettings;
StrPCopy(Buf, Setting);
if BuildCommDCB(Buf, DCB) <> 0 then
Exit;
DCB.Flags := DCB.Flags or dcbBinary;
DCB.Id := FCommId;
SetCommState(DCB);
end;
end;
{ ----------------------------------------------------------------------- }
{ Component registration }
{ ----------------------------------------------------------------------- }
procedure Register;
begin
RegisterComponents('KP', [TKPComm]);
end;
end.