Encryption and other serial fixes. Terminal cursor not being erased fixed. Ability to auto-size windows added.
This commit is contained in:
parent
79b2825a98
commit
3cc942a4d4
13 changed files with 579 additions and 57 deletions
|
|
@ -8,7 +8,7 @@ memsize = 64
|
|||
quit warning = false
|
||||
|
||||
[cpu]
|
||||
core = auto
|
||||
core = normal
|
||||
cputype = 486
|
||||
cycles = 33445
|
||||
|
||||
|
|
@ -27,6 +27,9 @@ umb = true
|
|||
xms = true
|
||||
ems = true
|
||||
|
||||
[serial]
|
||||
serial1 = nullmodem server:127.0.0.1 port:2323 transparent:1
|
||||
|
||||
[autoexec]
|
||||
mount c .
|
||||
c:
|
||||
|
|
|
|||
43
dvx/dvxApp.c
43
dvx/dvxApp.c
|
|
@ -1,6 +1,8 @@
|
|||
// dvx_app.c — Layer 5: Application API for DV/X GUI
|
||||
|
||||
#include "dvxApp.h"
|
||||
#include "dvxWidget.h"
|
||||
#include "widgets/widgetInternal.h"
|
||||
#include "dvxFont.h"
|
||||
#include "dvxCursor.h"
|
||||
|
||||
|
|
@ -355,6 +357,44 @@ void dvxDestroyWindow(AppContextT *ctx, WindowT *win) {
|
|||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// dvxFitWindow
|
||||
// ============================================================
|
||||
|
||||
void dvxFitWindow(AppContextT *ctx, WindowT *win) {
|
||||
if (!ctx || !win || !win->widgetRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Measure the widget tree to get minimum content size
|
||||
widgetCalcMinSizeTree(win->widgetRoot, &ctx->font);
|
||||
|
||||
int32_t contentW = win->widgetRoot->calcMinW;
|
||||
int32_t contentH = win->widgetRoot->calcMinH;
|
||||
|
||||
// Compute chrome overhead
|
||||
int32_t topChrome = CHROME_TOTAL_TOP;
|
||||
if (win->menuBar) {
|
||||
topChrome += CHROME_MENU_HEIGHT;
|
||||
}
|
||||
|
||||
int32_t newW = contentW + CHROME_TOTAL_SIDE * 2;
|
||||
int32_t newH = contentH + topChrome + CHROME_TOTAL_BOTTOM;
|
||||
|
||||
// Dirty old position
|
||||
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
|
||||
|
||||
// Resize
|
||||
win->w = newW;
|
||||
win->h = newH;
|
||||
wmUpdateContentRect(win);
|
||||
wmReallocContentBuf(win, &ctx->display);
|
||||
|
||||
// Dirty new position
|
||||
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// dvxGetBlitOps
|
||||
// ============================================================
|
||||
|
|
@ -506,8 +546,9 @@ bool dvxUpdate(AppContextT *ctx) {
|
|||
|
||||
if (ctx->dirty.count > 0) {
|
||||
compositeAndFlush(ctx);
|
||||
} else if (ctx->idleCallback) {
|
||||
ctx->idleCallback(ctx->idleCtx);
|
||||
} else {
|
||||
// Nothing to do — yield timeslice
|
||||
__dpmi_yield();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ typedef struct AppContextT {
|
|||
int32_t lastCloseClickId; // window ID of last-clicked close gadget (-1 = none)
|
||||
int32_t iconRefreshIdx; // next minimized icon to refresh (staggered)
|
||||
int32_t frameCount; // frame counter for periodic tasks
|
||||
void (*idleCallback)(void *ctx); // called instead of yield when non-NULL
|
||||
void *idleCtx;
|
||||
} AppContextT;
|
||||
|
||||
// Initialize the application (VESA mode, input, etc.)
|
||||
|
|
@ -60,6 +62,9 @@ WindowT *dvxCreateWindow(AppContextT *ctx, const char *title, int32_t x, int32_t
|
|||
// Destroy a window
|
||||
void dvxDestroyWindow(AppContextT *ctx, WindowT *win);
|
||||
|
||||
// Resize a window to fit its widget tree's minimum size
|
||||
void dvxFitWindow(AppContextT *ctx, WindowT *win);
|
||||
|
||||
// Invalidate a region of a window's content area (triggers repaint)
|
||||
void dvxInvalidateRect(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h);
|
||||
|
||||
|
|
|
|||
|
|
@ -307,6 +307,10 @@ typedef struct WidgetT {
|
|||
int32_t scrollbackCount; // current number of lines stored
|
||||
int32_t scrollbackHead; // write position (circular index)
|
||||
int32_t scrollPos; // view position (scrollbackCount = live)
|
||||
// Dirty tracking for fast repaint
|
||||
uint32_t dirtyRows; // bitmask of rows needing repaint
|
||||
int32_t lastCursorRow; // cursor row at last repaint
|
||||
int32_t lastCursorCol; // cursor col at last repaint
|
||||
// Communications interface (all NULL = disconnected)
|
||||
void *commCtx;
|
||||
int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen);
|
||||
|
|
@ -495,6 +499,11 @@ void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines);
|
|||
// Poll the comm interface for incoming data and process it. Returns bytes processed.
|
||||
int32_t wgtAnsiTermPoll(WidgetT *w);
|
||||
|
||||
// Fast repaint: renders only dirty rows directly into the window's content
|
||||
// buffer, bypassing the full widget paint pipeline. Returns number of rows
|
||||
// repainted (0 if nothing was dirty).
|
||||
int32_t wgtAnsiTermRepaint(WidgetT *w);
|
||||
|
||||
// ============================================================
|
||||
// Operations
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ static const int32_t sAnsiToCga[8] = { 0, 4, 2, 6, 1, 5, 3, 7 };
|
|||
|
||||
static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow);
|
||||
static void ansiTermDeleteLines(WidgetT *w, int32_t count);
|
||||
static void ansiTermDirtyAll(WidgetT *w);
|
||||
static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count);
|
||||
static void ansiTermDirtyRow(WidgetT *w, int32_t row);
|
||||
static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd);
|
||||
static void ansiTermEraseDisplay(WidgetT *w, int32_t mode);
|
||||
static void ansiTermEraseLine(WidgetT *w, int32_t mode);
|
||||
|
|
@ -93,6 +96,43 @@ static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow) {
|
|||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// ansiTermDirtyAll
|
||||
// ============================================================
|
||||
|
||||
static void ansiTermDirtyAll(WidgetT *w) {
|
||||
w->as.ansiTerm.dirtyRows = 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// ansiTermDirtyRange
|
||||
// ============================================================
|
||||
//
|
||||
// Mark rows dirty that are touched by a cell range [startCell, startCell+count).
|
||||
|
||||
static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count) {
|
||||
int32_t cols = w->as.ansiTerm.cols;
|
||||
int32_t startRow = startCell / cols;
|
||||
int32_t endRow = (startCell + count - 1) / cols;
|
||||
|
||||
for (int32_t r = startRow; r <= endRow && r < 32; r++) {
|
||||
w->as.ansiTerm.dirtyRows |= (1U << r);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// ansiTermDirtyRow
|
||||
// ============================================================
|
||||
|
||||
static void ansiTermDirtyRow(WidgetT *w, int32_t row) {
|
||||
if (row >= 0 && row < 32) {
|
||||
w->as.ansiTerm.dirtyRows |= (1U << row);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// ansiTermDeleteLines
|
||||
// ============================================================
|
||||
|
|
@ -122,6 +162,11 @@ static void ansiTermDeleteLines(WidgetT *w, int32_t count) {
|
|||
for (int32_t r = rows - count; r < rows; r++) {
|
||||
ansiTermFillCells(w, r * cols, cols);
|
||||
}
|
||||
|
||||
// All rows from cursorRow down are affected
|
||||
for (int32_t r = row; r < rows; r++) {
|
||||
ansiTermDirtyRow(w, r);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -383,6 +428,8 @@ static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) {
|
|||
cells[(start + i) * 2] = ' ';
|
||||
cells[(start + i) * 2 + 1] = attr;
|
||||
}
|
||||
|
||||
ansiTermDirtyRange(w, start, count);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -442,6 +489,11 @@ static void ansiTermInsertLines(WidgetT *w, int32_t count) {
|
|||
for (int32_t r = row; r < row + count; r++) {
|
||||
ansiTermFillCells(w, r * cols, cols);
|
||||
}
|
||||
|
||||
// All rows from cursorRow down are affected
|
||||
for (int32_t r = row; r < rows; r++) {
|
||||
ansiTermDirtyRow(w, r);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -610,6 +662,7 @@ static void ansiTermPutChar(WidgetT *w, uint8_t ch) {
|
|||
int32_t idx = (row * cols + col) * 2;
|
||||
w->as.ansiTerm.cells[idx] = ch;
|
||||
w->as.ansiTerm.cells[idx + 1] = w->as.ansiTerm.curAttr;
|
||||
ansiTermDirtyRow(w, row);
|
||||
}
|
||||
|
||||
w->as.ansiTerm.cursorCol++;
|
||||
|
|
@ -640,6 +693,8 @@ static void ansiTermScrollDown(WidgetT *w) {
|
|||
|
||||
// Clear the top line
|
||||
ansiTermFillCells(w, 0, cols);
|
||||
|
||||
ansiTermDirtyAll(w);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -672,6 +727,8 @@ static void ansiTermScrollUp(WidgetT *w) {
|
|||
if (wasAtBottom) {
|
||||
w->as.ansiTerm.scrollPos = w->as.ansiTerm.scrollbackCount;
|
||||
}
|
||||
|
||||
ansiTermDirtyAll(w);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -833,6 +890,119 @@ int32_t wgtAnsiTermPoll(WidgetT *w) {
|
|||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtAnsiTermRepaint
|
||||
// ============================================================
|
||||
//
|
||||
// Fast repaint: renders only dirty rows directly into the window's
|
||||
// content buffer, bypassing the full widget paint pipeline (no clear,
|
||||
// no relayout, no other widgets). This keeps ACK turnaround fast
|
||||
// for the serial link.
|
||||
|
||||
int32_t wgtAnsiTermRepaint(WidgetT *w) {
|
||||
if (!w || w->type != WidgetAnsiTermE || !w->window) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Always repaint the row the cursor was on last time (to erase it)
|
||||
// and the row it's on now (to draw it)
|
||||
int32_t prevRow = w->as.ansiTerm.lastCursorRow;
|
||||
int32_t curRow = w->as.ansiTerm.cursorRow;
|
||||
int32_t prevCol = w->as.ansiTerm.lastCursorCol;
|
||||
int32_t curCol = w->as.ansiTerm.cursorCol;
|
||||
|
||||
if (prevRow != curRow || prevCol != curCol) {
|
||||
if (prevRow >= 0 && prevRow < w->as.ansiTerm.rows) {
|
||||
w->as.ansiTerm.dirtyRows |= (1U << prevRow);
|
||||
}
|
||||
if (curRow >= 0 && curRow < w->as.ansiTerm.rows) {
|
||||
w->as.ansiTerm.dirtyRows |= (1U << curRow);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t dirty = w->as.ansiTerm.dirtyRows;
|
||||
if (dirty == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
WindowT *win = w->window;
|
||||
if (!win->contentBuf || !win->widgetRoot) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
AppContextT *ctx = (AppContextT *)win->widgetRoot->userData;
|
||||
if (!ctx) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Set up display context pointing at the content buffer
|
||||
DisplayT cd = ctx->display;
|
||||
cd.backBuf = win->contentBuf;
|
||||
cd.width = win->contentW;
|
||||
cd.height = win->contentH;
|
||||
cd.pitch = win->contentPitch;
|
||||
cd.clipX = 0;
|
||||
cd.clipY = 0;
|
||||
cd.clipW = win->contentW;
|
||||
cd.clipH = win->contentH;
|
||||
|
||||
const BlitOpsT *ops = &ctx->blitOps;
|
||||
const BitmapFontT *font = &ctx->font;
|
||||
|
||||
int32_t cols = w->as.ansiTerm.cols;
|
||||
int32_t rows = w->as.ansiTerm.rows;
|
||||
int32_t cellW = font->charWidth;
|
||||
int32_t cellH = font->charHeight;
|
||||
int32_t baseX = w->x + ANSI_BORDER;
|
||||
int32_t baseY = w->y + ANSI_BORDER;
|
||||
|
||||
// Build palette
|
||||
uint32_t palette[16];
|
||||
for (int32_t i = 0; i < 16; i++) {
|
||||
palette[i] = packColor(&cd, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]);
|
||||
}
|
||||
|
||||
bool viewingLive = (w->as.ansiTerm.scrollPos == w->as.ansiTerm.scrollbackCount);
|
||||
int32_t repainted = 0;
|
||||
|
||||
for (int32_t row = 0; row < rows; row++) {
|
||||
if (!(dirty & (1U << row))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t lineIndex = w->as.ansiTerm.scrollPos + row;
|
||||
const uint8_t *lineData = ansiTermGetLine(w, lineIndex);
|
||||
|
||||
for (int32_t col = 0; col < cols; col++) {
|
||||
uint8_t ch = lineData[col * 2];
|
||||
uint8_t attr = lineData[col * 2 + 1];
|
||||
|
||||
uint32_t fg = palette[attr & 0x0F];
|
||||
uint32_t bg = palette[(attr >> 4) & 0x0F];
|
||||
|
||||
if (viewingLive && w->as.ansiTerm.cursorVisible && w->focused &&
|
||||
row == w->as.ansiTerm.cursorRow && col == w->as.ansiTerm.cursorCol) {
|
||||
uint32_t tmp = fg;
|
||||
fg = bg;
|
||||
bg = tmp;
|
||||
}
|
||||
|
||||
int32_t cx = baseX + col * cellW;
|
||||
int32_t cy = baseY + row * cellH;
|
||||
|
||||
drawChar(&cd, ops, font, cx, cy, (char)ch, fg, bg, true);
|
||||
}
|
||||
|
||||
repainted++;
|
||||
}
|
||||
|
||||
w->as.ansiTerm.dirtyRows = 0;
|
||||
w->as.ansiTerm.lastCursorRow = w->as.ansiTerm.cursorRow;
|
||||
w->as.ansiTerm.lastCursorCol = w->as.ansiTerm.cursorCol;
|
||||
return repainted;
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// wgtAnsiTermSetComm
|
||||
// ============================================================
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <pc.h>
|
||||
#include "packet.h"
|
||||
#include "../rs232/rs232.h"
|
||||
|
|
@ -46,8 +47,8 @@
|
|||
// Receive buffer must hold at least one max-size stuffed frame
|
||||
#define RX_BUF_SIZE (MAX_STUFFED_SIZE + 64)
|
||||
|
||||
// Retransmit timeout in poll cycles (caller-dependent; ~50ms worth at typical poll rates)
|
||||
#define RETRANSMIT_TIMEOUT 500
|
||||
// Retransmit timeout in milliseconds
|
||||
#define RETRANSMIT_TIMEOUT_MS 500
|
||||
|
||||
// Receive state machine
|
||||
#define RX_STATE_HUNT 0 // scanning for FLAG_BYTE
|
||||
|
|
@ -64,7 +65,7 @@ typedef struct {
|
|||
uint8_t data[PKT_MAX_PAYLOAD];
|
||||
int len;
|
||||
uint8_t seq;
|
||||
uint32_t timer;
|
||||
clock_t timer;
|
||||
} TxSlotT;
|
||||
|
||||
// Connection state
|
||||
|
|
@ -86,8 +87,6 @@ struct PktConnS {
|
|||
uint8_t rxFrame[MAX_FRAME_SIZE];
|
||||
int rxFrameLen;
|
||||
|
||||
// Poll counter (simple timer)
|
||||
uint32_t pollCount;
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -203,6 +202,9 @@ static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *
|
|||
}
|
||||
}
|
||||
|
||||
// Trailing flag to close the frame immediately
|
||||
stuffed[out++] = FLAG_BYTE;
|
||||
|
||||
// Send via serial port (blocking write)
|
||||
rs232Write(conn->com, (const char *)stuffed, out);
|
||||
}
|
||||
|
|
@ -309,7 +311,7 @@ static void processFrame(PktConnT *conn, const uint8_t *frame, int len) {
|
|||
for (int i = idx; i < conn->txCount; i++) {
|
||||
TxSlotT *slot = &conn->txSlots[i];
|
||||
sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len);
|
||||
slot->timer = conn->pollCount;
|
||||
slot->timer = clock();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -378,11 +380,14 @@ static void rxProcessByte(PktConnT *conn, uint8_t byte) {
|
|||
// ========================================================================
|
||||
|
||||
static void retransmitCheck(PktConnT *conn) {
|
||||
clock_t now = clock();
|
||||
clock_t timeout = (clock_t)RETRANSMIT_TIMEOUT_MS * CLOCKS_PER_SEC / 1000;
|
||||
|
||||
for (int i = 0; i < conn->txCount; i++) {
|
||||
TxSlotT *slot = &conn->txSlots[i];
|
||||
if (conn->pollCount - slot->timer >= RETRANSMIT_TIMEOUT) {
|
||||
if (now - slot->timer >= timeout) {
|
||||
sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len);
|
||||
slot->timer = conn->pollCount;
|
||||
slot->timer = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -399,6 +404,14 @@ void pktClose(PktConnT *conn) {
|
|||
}
|
||||
|
||||
|
||||
bool pktCanSend(PktConnT *conn) {
|
||||
if (!conn) {
|
||||
return false;
|
||||
}
|
||||
return conn->txCount < conn->windowSize;
|
||||
}
|
||||
|
||||
|
||||
int pktGetPending(PktConnT *conn) {
|
||||
if (!conn) {
|
||||
return PKT_ERR_INVALID_PARAM;
|
||||
|
|
@ -432,7 +445,6 @@ PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *call
|
|||
conn->rxExpectSeq = 0;
|
||||
conn->rxState = RX_STATE_HUNT;
|
||||
conn->rxFrameLen = 0;
|
||||
conn->pollCount = 0;
|
||||
|
||||
return conn;
|
||||
}
|
||||
|
|
@ -447,8 +459,6 @@ int pktPoll(PktConnT *conn) {
|
|||
return PKT_ERR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
conn->pollCount++;
|
||||
|
||||
// Read available serial data and feed to state machine
|
||||
while ((nRead = rs232Read(conn->com, buf, sizeof(buf))) > 0) {
|
||||
for (int i = 0; i < nRead; i++) {
|
||||
|
|
@ -460,6 +470,11 @@ int pktPoll(PktConnT *conn) {
|
|||
}
|
||||
}
|
||||
|
||||
// Detect disconnected socket/port
|
||||
if (nRead < 0) {
|
||||
return PKT_ERR_DISCONNECTED;
|
||||
}
|
||||
|
||||
// Check for retransmit timeouts
|
||||
retransmitCheck(conn);
|
||||
|
||||
|
|
@ -498,7 +513,9 @@ int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) {
|
|||
// Wait for window space if blocking
|
||||
if (block) {
|
||||
while (conn->txCount >= conn->windowSize) {
|
||||
pktPoll(conn);
|
||||
if (pktPoll(conn) == PKT_ERR_DISCONNECTED) {
|
||||
return PKT_ERR_DISCONNECTED;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (conn->txCount >= conn->windowSize) {
|
||||
|
|
@ -511,7 +528,7 @@ int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) {
|
|||
memcpy(slot->data, data, len);
|
||||
slot->len = len;
|
||||
slot->seq = conn->txNextSeq;
|
||||
slot->timer = conn->pollCount;
|
||||
slot->timer = clock();
|
||||
conn->txCount++;
|
||||
conn->txNextSeq++;
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
#define PKT_ERR_INVALID_PARAM -6
|
||||
#define PKT_ERR_TX_FULL -7
|
||||
#define PKT_ERR_NO_DATA -8
|
||||
#define PKT_ERR_DISCONNECTED -9
|
||||
|
||||
// Callback for received packets
|
||||
// ctx: user context pointer
|
||||
|
|
@ -62,6 +63,9 @@ int pktPoll(PktConnT *conn);
|
|||
// Sends a RST frame to the remote side.
|
||||
int pktReset(PktConnT *conn);
|
||||
|
||||
// Check if there is room in the transmit window.
|
||||
bool pktCanSend(PktConnT *conn);
|
||||
|
||||
// Get number of unacknowledged packets in the transmit window.
|
||||
int pktGetPending(PktConnT *conn);
|
||||
|
||||
|
|
|
|||
273
proxy/proxy.c
273
proxy/proxy.c
|
|
@ -4,7 +4,7 @@
|
|||
// secLink protocol plain telnet
|
||||
//
|
||||
// Usage: proxy [listen_port] [bbs_host] [bbs_port]
|
||||
// Defaults: 2323 bbs.duensing.digital 23
|
||||
// Defaults: 2323 10.1.0.244 2023
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -28,18 +28,46 @@
|
|||
// ========================================================================
|
||||
|
||||
#define DEFAULT_LISTEN_PORT 2323
|
||||
#define DEFAULT_BBS_HOST "bbs.duensing.digital"
|
||||
#define DEFAULT_BBS_PORT 23
|
||||
#define DEFAULT_BBS_HOST "10.1.0.244"
|
||||
#define DEFAULT_BBS_PORT 2023
|
||||
|
||||
#define CHANNEL_TERMINAL 0
|
||||
#define POLL_TIMEOUT_MS 10
|
||||
|
||||
// Telnet protocol bytes
|
||||
#define TEL_IAC 255
|
||||
#define TEL_DONT 254
|
||||
#define TEL_DO 253
|
||||
#define TEL_WONT 252
|
||||
#define TEL_WILL 251
|
||||
#define TEL_SB 250
|
||||
#define TEL_SE 240
|
||||
|
||||
// Telnet options we accept
|
||||
#define TELOPT_ECHO 1
|
||||
#define TELOPT_SGA 3
|
||||
#define TELOPT_TTYPE 24
|
||||
#define TELOPT_NAWS 31
|
||||
|
||||
// Telnet parser states
|
||||
#define TS_DATA 0
|
||||
#define TS_IAC 1
|
||||
#define TS_WILL 2
|
||||
#define TS_WONT 3
|
||||
#define TS_DO 4
|
||||
#define TS_DONT 5
|
||||
#define TS_SB 6
|
||||
#define TS_SB_IAC 7
|
||||
|
||||
|
||||
// ========================================================================
|
||||
// Static globals
|
||||
// ========================================================================
|
||||
|
||||
static volatile bool sRunning = true;
|
||||
static volatile bool sGotEnter = false;
|
||||
static int sTelState = TS_DATA;
|
||||
static int sClientFd = -1;
|
||||
|
||||
|
||||
// ========================================================================
|
||||
|
|
@ -48,9 +76,12 @@ static volatile bool sRunning = true;
|
|||
|
||||
static int connectToBbs(const char *host, int port);
|
||||
static int createListenSocket(int port);
|
||||
static void hexDump(const char *label, const uint8_t *data, int len);
|
||||
static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel);
|
||||
static void seedRng(void);
|
||||
static void sigHandler(int sig);
|
||||
static void telnetRespond(int bbsFd, uint8_t cmd, uint8_t opt);
|
||||
static int telnetFilter(int bbsFd, const uint8_t *in, int inLen, uint8_t *out);
|
||||
|
||||
|
||||
// ========================================================================
|
||||
|
|
@ -121,11 +152,42 @@ static int createListenSocket(int port) {
|
|||
}
|
||||
|
||||
|
||||
static void hexDump(const char *label, const uint8_t *data, int len) {
|
||||
printf("%s (%d bytes):", label, len);
|
||||
for (int i = 0; i < len && i < 64; i++) {
|
||||
if (i % 16 == 0) {
|
||||
printf("\n ");
|
||||
}
|
||||
printf("%02X ", data[i]);
|
||||
}
|
||||
if (len > 64) {
|
||||
printf("\n ...");
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
|
||||
|
||||
static void onRecvFromDos(void *ctx, const uint8_t *data, int len, uint8_t channel) {
|
||||
int bbsFd = *(int *)ctx;
|
||||
|
||||
(void)channel;
|
||||
|
||||
hexDump("DOS->proxy (decrypted)", data, len);
|
||||
|
||||
// Check for ENTER before BBS is connected
|
||||
if (!sGotEnter) {
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (data[i] == '\r' || data[i] == '\n') {
|
||||
sGotEnter = true;
|
||||
printf("Got ENTER from terminal.\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
hexDump("DOS->BBS", data, len);
|
||||
|
||||
int sent = 0;
|
||||
while (sent < len) {
|
||||
ssize_t n = write(bbsFd, data + sent, len - sent);
|
||||
|
|
@ -156,6 +218,123 @@ static void seedRng(void) {
|
|||
static void sigHandler(int sig) {
|
||||
(void)sig;
|
||||
sRunning = false;
|
||||
// Shutdown the client socket to break any blocking reads in the packet layer
|
||||
if (sClientFd >= 0) {
|
||||
shutdown(sClientFd, SHUT_RDWR);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Send a telnet negotiation response
|
||||
static void telnetRespond(int bbsFd, uint8_t cmd, uint8_t opt) {
|
||||
uint8_t resp[3] = {TEL_IAC, cmd, opt};
|
||||
ssize_t n = write(bbsFd, resp, 3);
|
||||
(void)n;
|
||||
printf(" TEL TX: %s %d\n",
|
||||
cmd == TEL_WILL ? "WILL" : cmd == TEL_WONT ? "WONT" :
|
||||
cmd == TEL_DO ? "DO" : "DONT", opt);
|
||||
}
|
||||
|
||||
|
||||
// Filter telnet IAC sequences from BBS data.
|
||||
// Handles negotiation by responding appropriately.
|
||||
// Returns the number of clean data bytes written to 'out'.
|
||||
static int telnetFilter(int bbsFd, const uint8_t *in, int inLen, uint8_t *out) {
|
||||
int outLen = 0;
|
||||
|
||||
for (int i = 0; i < inLen; i++) {
|
||||
uint8_t b = in[i];
|
||||
|
||||
switch (sTelState) {
|
||||
case TS_DATA:
|
||||
if (b == TEL_IAC) {
|
||||
sTelState = TS_IAC;
|
||||
} else {
|
||||
out[outLen++] = b;
|
||||
}
|
||||
break;
|
||||
|
||||
case TS_IAC:
|
||||
switch (b) {
|
||||
case TEL_IAC:
|
||||
// Escaped 0xFF — emit literal
|
||||
out[outLen++] = 0xFF;
|
||||
sTelState = TS_DATA;
|
||||
break;
|
||||
case TEL_WILL:
|
||||
sTelState = TS_WILL;
|
||||
break;
|
||||
case TEL_WONT:
|
||||
sTelState = TS_WONT;
|
||||
break;
|
||||
case TEL_DO:
|
||||
sTelState = TS_DO;
|
||||
break;
|
||||
case TEL_DONT:
|
||||
sTelState = TS_DONT;
|
||||
break;
|
||||
case TEL_SB:
|
||||
sTelState = TS_SB;
|
||||
break;
|
||||
default:
|
||||
// Unknown command, skip
|
||||
sTelState = TS_DATA;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case TS_WILL:
|
||||
// Server offers to do something — accept ECHO and SGA, refuse others
|
||||
printf(" TEL RX: WILL %d\n", b);
|
||||
if (b == TELOPT_ECHO || b == TELOPT_SGA) {
|
||||
telnetRespond(bbsFd, TEL_DO, b);
|
||||
} else {
|
||||
telnetRespond(bbsFd, TEL_DONT, b);
|
||||
}
|
||||
sTelState = TS_DATA;
|
||||
break;
|
||||
|
||||
case TS_WONT:
|
||||
printf(" TEL RX: WONT %d\n", b);
|
||||
telnetRespond(bbsFd, TEL_DONT, b);
|
||||
sTelState = TS_DATA;
|
||||
break;
|
||||
|
||||
case TS_DO:
|
||||
// Server asks us to do something — accept TTYPE and NAWS, refuse others
|
||||
printf(" TEL RX: DO %d\n", b);
|
||||
if (b == TELOPT_TTYPE || b == TELOPT_NAWS) {
|
||||
telnetRespond(bbsFd, TEL_WILL, b);
|
||||
} else {
|
||||
telnetRespond(bbsFd, TEL_WONT, b);
|
||||
}
|
||||
sTelState = TS_DATA;
|
||||
break;
|
||||
|
||||
case TS_DONT:
|
||||
printf(" TEL RX: DONT %d\n", b);
|
||||
telnetRespond(bbsFd, TEL_WONT, b);
|
||||
sTelState = TS_DATA;
|
||||
break;
|
||||
|
||||
case TS_SB:
|
||||
// Inside subnegotiation — skip until IAC SE
|
||||
if (b == TEL_IAC) {
|
||||
sTelState = TS_SB_IAC;
|
||||
}
|
||||
break;
|
||||
|
||||
case TS_SB_IAC:
|
||||
if (b == TEL_SE) {
|
||||
sTelState = TS_DATA;
|
||||
} else {
|
||||
sTelState = TS_SB;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return outLen;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -193,8 +372,12 @@ int main(int argc, char *argv[]) {
|
|||
}
|
||||
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
signal(SIGINT, sigHandler);
|
||||
signal(SIGTERM, sigHandler);
|
||||
struct sigaction sa;
|
||||
sa.sa_handler = sigHandler;
|
||||
sa.sa_flags = 0;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sigaction(SIGINT, &sa, NULL);
|
||||
sigaction(SIGTERM, &sa, NULL);
|
||||
|
||||
// Listen for 86Box connection
|
||||
listenFd = createListenSocket(listenPort);
|
||||
|
|
@ -204,35 +387,34 @@ int main(int argc, char *argv[]) {
|
|||
}
|
||||
printf("Listening on port %d...\n", listenPort);
|
||||
|
||||
// Accept connection from 86Box
|
||||
// Accept connection from 86Box (poll so Ctrl+C works)
|
||||
struct pollfd listenPoll = {listenFd, POLLIN, 0};
|
||||
while (sRunning) {
|
||||
int pr = poll(&listenPoll, 1, 500);
|
||||
if (pr > 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
clientLen = sizeof(clientAddr);
|
||||
clientFd = accept(listenFd, (struct sockaddr *)&clientAddr, &clientLen);
|
||||
close(listenFd);
|
||||
if (clientFd < 0) {
|
||||
if (!sRunning || clientFd < 0) {
|
||||
if (sRunning) {
|
||||
fprintf(stderr, "Accept failed: %s\n", strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
return sRunning ? 1 : 0;
|
||||
}
|
||||
printf("86Box connected.\n");
|
||||
sClientFd = clientFd;
|
||||
|
||||
// Associate socket with COM0 for the secLink stack
|
||||
sockShimSetFd(0, clientFd);
|
||||
|
||||
// Connect to BBS
|
||||
printf("Connecting to %s:%d...\n", bbsHost, bbsPort);
|
||||
bbsFd = connectToBbs(bbsHost, bbsPort);
|
||||
if (bbsFd < 0) {
|
||||
fprintf(stderr, "Failed to connect to BBS: %s\n", strerror(errno));
|
||||
close(clientFd);
|
||||
return 1;
|
||||
}
|
||||
printf("BBS connected.\n");
|
||||
|
||||
// Seed RNG from /dev/urandom and open secLink
|
||||
seedRng();
|
||||
link = secLinkOpen(0, 115200, 8, 'N', 1, 0, onRecvFromDos, &bbsFd);
|
||||
if (!link) {
|
||||
fprintf(stderr, "Failed to open secLink.\n");
|
||||
close(bbsFd);
|
||||
close(clientFd);
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -243,10 +425,38 @@ int main(int argc, char *argv[]) {
|
|||
if (rc != SECLINK_SUCCESS) {
|
||||
fprintf(stderr, "Handshake failed: %d\n", rc);
|
||||
secLinkClose(link);
|
||||
close(bbsFd);
|
||||
return 1;
|
||||
}
|
||||
printf("Handshake complete. Proxying traffic.\n");
|
||||
printf("Handshake complete.\n");
|
||||
|
||||
// Wait for ENTER from terminal before connecting to BBS
|
||||
printf("Waiting for terminal to send ENTER...\n");
|
||||
while (sRunning) {
|
||||
secLinkPoll(link);
|
||||
if (sGotEnter) {
|
||||
break;
|
||||
}
|
||||
usleep(10000);
|
||||
}
|
||||
if (!sRunning) {
|
||||
secLinkClose(link);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Send test string to verify data path
|
||||
const char *testMsg = "SecLink proxy ready.\r\n";
|
||||
secLinkSend(link, (const uint8_t *)testMsg, (int)strlen(testMsg), CHANNEL_TERMINAL, true, false);
|
||||
printf("Sent test message to terminal.\n");
|
||||
|
||||
// Now connect to BBS
|
||||
printf("Connecting to %s:%d...\n", bbsHost, bbsPort);
|
||||
bbsFd = connectToBbs(bbsHost, bbsPort);
|
||||
if (bbsFd < 0) {
|
||||
fprintf(stderr, "Failed to connect to BBS: %s\n", strerror(errno));
|
||||
secLinkClose(link);
|
||||
return 1;
|
||||
}
|
||||
printf("BBS connected. Proxying traffic.\n");
|
||||
|
||||
// Set BBS socket non-blocking for the main loop
|
||||
int flags = fcntl(bbsFd, F_GETFL, 0);
|
||||
|
|
@ -265,15 +475,28 @@ int main(int argc, char *argv[]) {
|
|||
// (callback forwards decrypted data to BBS)
|
||||
secLinkPoll(link);
|
||||
|
||||
// Read from BBS and send encrypted to 86Box
|
||||
// Read from BBS, filter telnet, send clean data to 86Box
|
||||
if (fds[1].revents & POLLIN) {
|
||||
uint8_t buf[SECLINK_MAX_PAYLOAD];
|
||||
ssize_t n = read(bbsFd, buf, sizeof(buf));
|
||||
uint8_t raw[64];
|
||||
uint8_t clean[64];
|
||||
ssize_t n = read(bbsFd, raw, sizeof(raw));
|
||||
if (n <= 0) {
|
||||
printf("BBS disconnected.\n");
|
||||
break;
|
||||
}
|
||||
secLinkSend(link, buf, (int)n, CHANNEL_TERMINAL, true, true);
|
||||
|
||||
int cleanLen = telnetFilter(bbsFd, raw, (int)n, clean);
|
||||
if (cleanLen > 0) {
|
||||
hexDump("BBS->DOS", clean, cleanLen);
|
||||
|
||||
// Retry with ACK processing if the send window is full
|
||||
int rc = secLinkSend(link, clean, cleanLen, CHANNEL_TERMINAL, true, false);
|
||||
while (rc != SECLINK_SUCCESS && sRunning) {
|
||||
secLinkPoll(link);
|
||||
usleep(1000);
|
||||
rc = secLinkSend(link, clean, cleanLen, CHANNEL_TERMINAL, true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for disconnects
|
||||
|
|
|
|||
|
|
@ -53,13 +53,19 @@ int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int
|
|||
|
||||
int rs232Read(int com, char *data, int len) {
|
||||
if (com < 0 || com >= MAX_PORTS || sFds[com] < 0) {
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
ssize_t n = recv(sFds[com], data, len, MSG_DONTWAIT);
|
||||
if (n <= 0) {
|
||||
if (n < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (n == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (int)n;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1321,11 +1321,11 @@ int rs232SetParity(int com, char parity) {
|
|||
}
|
||||
|
||||
switch (parity) {
|
||||
case 'n': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_NONE); return RS232_SUCCESS;
|
||||
case 'e': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_EVEN); return RS232_SUCCESS;
|
||||
case 'o': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_ODD); return RS232_SUCCESS;
|
||||
case 'm': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_MARK); return RS232_SUCCESS;
|
||||
case 's': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_SPACE); return RS232_SUCCESS;
|
||||
case 'n': case 'N': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_NONE); return RS232_SUCCESS;
|
||||
case 'e': case 'E': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_EVEN); return RS232_SUCCESS;
|
||||
case 'o': case 'O': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_ODD); return RS232_SUCCESS;
|
||||
case 'm': case 'M': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_MARK); return RS232_SUCCESS;
|
||||
case 's': case 'S': UART_WRITE_LCR(port, (UART_READ_LCR(port) & ~PARITY_MASK) | PARITY_SPACE); return RS232_SUCCESS;
|
||||
}
|
||||
return RS232_ERR_INVALID_PARITY;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ static void completeHandshake(SecLinkT *link) {
|
|||
uint8_t txKey[SEC_XTEA_KEY_SIZE];
|
||||
uint8_t rxKey[SEC_XTEA_KEY_SIZE];
|
||||
bool weAreLower;
|
||||
|
||||
secDhComputeSecret(link->dh, link->remoteKey, SEC_DH_KEY_SIZE);
|
||||
secDhDeriveKey(link->dh, masterKey, SEC_XTEA_KEY_SIZE);
|
||||
|
||||
|
|
@ -212,7 +211,9 @@ int secLinkHandshake(SecLinkT *link) {
|
|||
|
||||
// Poll until the callback completes the handshake
|
||||
while (link->state != STATE_READY) {
|
||||
pktPoll(link->pkt);
|
||||
if (pktPoll(link->pkt) == PKT_ERR_DISCONNECTED) {
|
||||
return SECLINK_ERR_HANDSHAKE;
|
||||
}
|
||||
}
|
||||
|
||||
return SECLINK_SUCCESS;
|
||||
|
|
@ -286,6 +287,12 @@ int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, b
|
|||
return SECLINK_ERR_PARAM;
|
||||
}
|
||||
|
||||
// For non-blocking sends, check window space BEFORE encrypting
|
||||
// to avoid advancing the cipher counter on a failed send
|
||||
if (!block && !pktCanSend(link->pkt)) {
|
||||
return SECLINK_ERR_SEND;
|
||||
}
|
||||
|
||||
// Build channel header byte
|
||||
buf[0] = channel & CHANNEL_MASK;
|
||||
if (encrypt) {
|
||||
|
|
|
|||
|
|
@ -339,8 +339,8 @@ static void computeR2(BigNumT *r2, const BigNumT *m) {
|
|||
bnSet(r2, 1);
|
||||
|
||||
for (int i = 0; i < 2 * BN_BITS; i++) {
|
||||
bnShiftLeft1(r2);
|
||||
if (bnCmp(r2, m) >= 0) {
|
||||
int carry = bnShiftLeft1(r2);
|
||||
if (carry || bnCmp(r2, m) >= 0) {
|
||||
bnSub(r2, r2, m);
|
||||
}
|
||||
}
|
||||
|
|
@ -586,6 +586,7 @@ int secDhGenerateKeys(SecDhT *dh) {
|
|||
|
||||
// public = g^private mod p
|
||||
bnModExp(&dh->publicKey, &sDhGenerator, &dh->privateKey, &sDhPrime, sDhM0Inv, &sDhR2);
|
||||
|
||||
dh->hasKeys = true;
|
||||
dh->hasSecret = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
#include "dvxWidget.h"
|
||||
#include "secLink.h"
|
||||
#include "security.h"
|
||||
#include "../rs232/rs232.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
|
@ -46,6 +47,7 @@ typedef struct {
|
|||
|
||||
static int32_t commRead(void *ctx, uint8_t *buf, int32_t maxLen);
|
||||
static int32_t commWrite(void *ctx, const uint8_t *data, int32_t len);
|
||||
static void idlePoll(void *ctx);
|
||||
static void onCloseCb(WindowT *win);
|
||||
static void onMenuCb(WindowT *win, int32_t menuId);
|
||||
static void onRecv(void *ctx, const uint8_t *data, int len, uint8_t channel);
|
||||
|
|
@ -80,6 +82,16 @@ static int32_t commWrite(void *ctx, const uint8_t *data, int32_t len) {
|
|||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// idlePoll — poll serial link instead of yielding CPU
|
||||
// ============================================================
|
||||
|
||||
static void idlePoll(void *ctx) {
|
||||
TermContextT *tc = (TermContextT *)ctx;
|
||||
secLinkPoll(tc->link);
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
// onCloseCb — quit when the terminal window is closed
|
||||
// ============================================================
|
||||
|
|
@ -178,6 +190,15 @@ int main(int argc, char *argv[]) {
|
|||
TermContextT tc;
|
||||
memset(&tc, 0, sizeof(tc));
|
||||
|
||||
// Test rs232 directly first for diagnostics
|
||||
printf("Testing rs232Open on COM%d...\n", comPort + 1);
|
||||
int rsRc = rs232Open(comPort, baudRate, 8, 'N', 1, 0);
|
||||
printf("rs232Open returned %d\n", rsRc);
|
||||
if (rsRc == RS232_SUCCESS) {
|
||||
printf("UART type: %d\n", rs232GetUartType(comPort));
|
||||
rs232Close(comPort);
|
||||
}
|
||||
|
||||
// Open secLink on the specified COM port
|
||||
printf("Opening SecLink on COM%d...\n", comPort + 1);
|
||||
tc.link = secLinkOpen(comPort, baudRate, 8, 'N', 1, 0, onRecv, &tc);
|
||||
|
|
@ -213,7 +234,7 @@ int main(int argc, char *argv[]) {
|
|||
tc.app = &ctx;
|
||||
|
||||
// Create the terminal window
|
||||
WindowT *win = dvxCreateWindow(&ctx, "SecLink Terminal", 40, 30, 680, 460, true);
|
||||
WindowT *win = dvxCreateWindow(&ctx, "SecLink Terminal", 40, 30, 680, 460, false);
|
||||
|
||||
if (!win) {
|
||||
dvxShutdown(&ctx);
|
||||
|
|
@ -253,12 +274,27 @@ int main(int argc, char *argv[]) {
|
|||
snprintf(statusText, sizeof(statusText), "COM%d %ld 8N1 [Encrypted]", comPort + 1, (long)baudRate);
|
||||
wgtLabel(sb, statusText);
|
||||
|
||||
// Fit window to widget tree
|
||||
dvxFitWindow(&ctx, win);
|
||||
wgtInvalidate(root);
|
||||
|
||||
// Main loop — poll secLink and terminal each frame
|
||||
while (dvxUpdate(&ctx)) {
|
||||
// Poll serial during idle instead of yielding CPU
|
||||
ctx.idleCallback = idlePoll;
|
||||
ctx.idleCtx = &tc;
|
||||
|
||||
// Main loop — poll serial, render immediately, composite
|
||||
while (ctx.running) {
|
||||
secLinkPoll(tc.link);
|
||||
wgtAnsiTermPoll(term);
|
||||
|
||||
if (wgtAnsiTermRepaint(term) > 0) {
|
||||
win->contentDirty = true;
|
||||
dvxInvalidateWindow(&ctx, win);
|
||||
}
|
||||
|
||||
if (!dvxUpdate(&ctx)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue