mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 18:43:51 +00:00
Реализованы все четыре потока §3.4 и рекордер:
• RX_AUDIO_STREAM — тап ДО громкости и мьюта (OnDemodAudioReady): скиммеру
и цифре нужен звук приёмника, а не то, что осталось после ручки;
• LINEOUT_STREAM — тап ПОСЛЕ (OnAudioReady), то есть что слышно;
• IQ_STREAM — тап сырого IQ в движке, ОДИН вызов на накопленный блок;
• TX_AUDIO_STREAM + TX_CHRONO — TRX:0,true,tci берёт модуляцию из потока
клиента (флаг TCIMicRequested впереди web в SetMOX), маркеры времени идут
из тика по часам, аудио клиента разворачивается в 48 кГц моно в тот же
ринг, что и web-микрофон;
• LINE_OUT_RECORDER_* — кольцо int16 на приёмник, WAV пишет отдельный поток.
Тапы аудио в контроллере многоадресные (AddAudioTap): слушают, ничего не
забирая, в отличие от OnAudioConsume, которым владеет web. Блоки нарезает и
раскладывает по кольцам клиентов сам DSP-поток, в сокет пишет поток клиента —
та же дисциплина, что у команд. Порядок локов везде FSliceLock → FStreamLock.
У очереди команд и кольца блоков разная политика переполнения: команду терять
нельзя, блок потока — можно (теряется самый старый).
Пересчёт частоты многоступенчатый (TCIStreams). Одноступенчатый FIR на верхнем
пресете Pluto (5760 кГц, коэффициент 120) упирался в потолок отводов и давал
завал 1.3 дБ в полосе при подавлении зеркала 16 дБ — то есть поток IQ с
мусором. Теперь коэффициент раскладывается на множители, спецификацию фильтра
каждой ступени задаёт ИТОГОВАЯ полоса, а свёртка идёт со сложением
симметричных пар: −83 дБ на любом коэффициенте, ≈10% ядра на 5.76 МГц.
Согласование частот с железом: из пресетов Pluto 576 и 960 кГц на 384 не
делятся, поэтому отдаём наибольшую ЗАКОННУЮ частоту, делящую источник нацело
(576/960 → 192 кГц). Ответ на IQ_SAMPLERATE называет достижимое, а не просьбу
клиента, и переобъявляется без запроса при смене rate и устройства.
Приёмный буфер соединения 4 → 32 КБ: блок TX-аудио это 64 байта заголовка плюс
data[16384], а кадр крупнее буфера не собирается никогда.
Настройки TCI переехали из Advanced на вкладку CAT, справа от TCP CAT Server:
это такой же канал внешнего управления трансивером.
Стенд (scratchpad, tcitest.pas): 112 проверок, все зелёные — включая сквозной
прогон через живой WDSP (синтетический IQ → блоки RX-аудио и IQ у настоящего
WS-клиента, и обратно TX-аудио клиента → блоки TX-IQ).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
196 lines
5.7 KiB
ObjectPascal
196 lines
5.7 KiB
ObjectPascal
unit WsClient;
|
|
|
|
{
|
|
WsClient.pas — WebSocket клиент (одно соединение).
|
|
|
|
Инкапсулирует:
|
|
- TCP-сокет
|
|
- Состояние WS (handshake / open / closed)
|
|
- Буфер приёма
|
|
- Отправку raw-байт, WS-фреймов (text / binary)
|
|
- Basic-Auth флаг
|
|
}
|
|
|
|
{$IFDEF FPC}
|
|
{$MODE Delphi}
|
|
{$LONGSTRINGS ON}
|
|
{$ENDIF}
|
|
|
|
interface
|
|
|
|
uses
|
|
SyncObjs, WebUtils
|
|
{$IFDEF WINDOWS}, WinSock2{$ELSE}, Sockets{$ENDIF};
|
|
|
|
const
|
|
{ Приёмный буфер соединения. 4 КБ хватало командам и web-запросам, но блок
|
|
бинарного потока TCI (§3.4) — это заголовок 64 байта плюс data[16384],
|
|
и кадр крупнее буфера не собирается НИКОГДА: BufLen упирается в потолок и
|
|
разбор встаёт. Поэтому потолок держим с запасом на кадр целиком вместе с
|
|
маской и хвостом соседнего сообщения. }
|
|
WS_BUF_SIZE = 32768;
|
|
|
|
type
|
|
TWsState = (wsHandshake, wsOpen, wsClosed);
|
|
|
|
TWsClient = class
|
|
private
|
|
FSocket: TSocket;
|
|
FState: TWsState;
|
|
FLock: TCriticalSection;
|
|
FBuf: array[0..WS_BUF_SIZE-1] of Byte;
|
|
FBufLen: Integer;
|
|
FAuthed: Boolean;
|
|
public
|
|
constructor Create(ASocket: TSocket);
|
|
destructor Destroy; override;
|
|
|
|
{ Отправка raw-байт (вызывать держа FLock) }
|
|
function SendRaw(const Data; Len: Integer): Boolean;
|
|
|
|
{ Отправка WebSocket-фрейма (text или binary) }
|
|
function SendWsFrame(Opcode: Byte; const Data; Len: Integer): Boolean;
|
|
|
|
{ Текстовый WS-фрейм (opcode $01) }
|
|
function SendText(const S: string): Boolean;
|
|
|
|
{ Бинарный WS-фрейм (opcode $02) }
|
|
function SendBinary(const Data; Len: Integer): Boolean;
|
|
|
|
{ Читает данные в FBuf, возвращает кол-во байт (-1 = ошибка/закрыто) }
|
|
function Recv: Integer;
|
|
|
|
{ Указатель на начало буфера приёма }
|
|
function BufData: PByte; inline;
|
|
|
|
{ Ёмкость приёмного буфера: разбору фреймов нужен потолок, чтобы вовремя
|
|
закрыть соединение, а не встать намертво на несобираемом кадре. }
|
|
function BufCapacity: Integer; inline;
|
|
|
|
property Socket: TSocket read FSocket;
|
|
property State: TWsState read FState write FState;
|
|
property Authed: Boolean read FAuthed write FAuthed;
|
|
property Lock: TCriticalSection read FLock;
|
|
property BufLen: Integer read FBufLen write FBufLen;
|
|
end;
|
|
|
|
implementation
|
|
|
|
{ ═══════════════════════════════════════════════════════════════════════════
|
|
TWsClient
|
|
═══════════════════════════════════════════════════════════════════════════ }
|
|
|
|
constructor TWsClient.Create(ASocket: TSocket);
|
|
begin
|
|
inherited Create;
|
|
FSocket := ASocket;
|
|
FState := wsHandshake;
|
|
FBufLen := 0;
|
|
FAuthed := False;
|
|
FLock := TCriticalSection.Create;
|
|
end;
|
|
|
|
destructor TWsClient.Destroy;
|
|
begin
|
|
if FSocket <> SOCK_INVALID then
|
|
SockClose(FSocket);
|
|
FLock.Free;
|
|
inherited;
|
|
end;
|
|
|
|
function TWsClient.SendRaw(const Data; Len: Integer): Boolean;
|
|
var
|
|
Sent, R: Integer;
|
|
P: PByte;
|
|
begin
|
|
Result := False;
|
|
if (FSocket = SOCK_INVALID) or (Len <= 0) then Exit;
|
|
P := @Data;
|
|
Sent := 0;
|
|
while Sent < Len do
|
|
begin
|
|
R := SockSend(FSocket, P + Sent, Len - Sent, 0);
|
|
if R <= 0 then Exit;
|
|
Inc(Sent, R);
|
|
end;
|
|
Result := True;
|
|
end;
|
|
|
|
function TWsClient.SendWsFrame(Opcode: Byte; const Data; Len: Integer): Boolean;
|
|
var
|
|
Header: array[0..9] of Byte;
|
|
HLen: Integer;
|
|
P: PByte;
|
|
begin
|
|
Result := False;
|
|
if FState <> wsOpen then Exit;
|
|
|
|
// FIN=1 + opcode
|
|
Header[0] := $80 or (Opcode and $0F);
|
|
|
|
if Len <= 125 then
|
|
begin
|
|
Header[1] := Byte(Len);
|
|
HLen := 2;
|
|
end
|
|
else if Len <= 65535 then
|
|
begin
|
|
Header[1] := 126;
|
|
Header[2] := Byte(Len shr 8);
|
|
Header[3] := Byte(Len);
|
|
HLen := 4;
|
|
end
|
|
else
|
|
begin
|
|
Header[1] := 127;
|
|
Header[2] := 0; Header[3] := 0; Header[4] := 0; Header[5] := 0;
|
|
Header[6] := Byte(Len shr 24); Header[7] := Byte(Len shr 16);
|
|
Header[8] := Byte(Len shr 8); Header[9] := Byte(Len);
|
|
HLen := 10;
|
|
end;
|
|
|
|
FLock.Enter;
|
|
try
|
|
Result := SendRaw(Header[0], HLen);
|
|
if Result and (Len > 0) then
|
|
begin
|
|
P := @Data;
|
|
Result := SendRaw(P^, Len);
|
|
end;
|
|
// Ошибка отправки (таймаут SO_SNDTIMEO, разрыв) — немедленно закрываем
|
|
// клиент чтобы BroadcastBinary не держал FClientLock на следующих тиках.
|
|
if not Result then FState := wsClosed;
|
|
finally
|
|
FLock.Leave;
|
|
end;
|
|
end;
|
|
|
|
function TWsClient.SendText(const S: string): Boolean;
|
|
begin
|
|
if Length(S) = 0 then begin Result := True; Exit; end;
|
|
Result := SendWsFrame($01, S[1], Length(S));
|
|
end;
|
|
|
|
function TWsClient.SendBinary(const Data; Len: Integer): Boolean;
|
|
begin
|
|
Result := SendWsFrame($02, Data, Len);
|
|
end;
|
|
|
|
function TWsClient.Recv: Integer;
|
|
begin
|
|
Result := SockRecv(FSocket, @FBuf[FBufLen], SizeOf(FBuf) - FBufLen, 0);
|
|
if Result > 0 then Inc(FBufLen, Result);
|
|
end;
|
|
|
|
function TWsClient.BufData: PByte;
|
|
begin
|
|
Result := @FBuf[0];
|
|
end;
|
|
|
|
function TWsClient.BufCapacity: Integer;
|
|
begin
|
|
Result := SizeOf(FBuf);
|
|
end;
|
|
|
|
end.
|