Updated docs.
This commit is contained in:
parent
5a1332d024
commit
26c3d7440d
12 changed files with 3216 additions and 2065 deletions
267
README.md
267
README.md
|
|
@ -1,50 +1,64 @@
|
|||
# DVX -- DOS Visual eXecutive
|
||||
|
||||
A Windows 3.x-style desktop shell for DOS, built with DJGPP/DPMI. Combines a
|
||||
windowed GUI compositor, cooperative task switcher, and DXE3 dynamic loading to
|
||||
create a multitasking desktop environment where applications are `.app` shared
|
||||
libraries loaded at runtime.
|
||||
A windowed GUI compositor and desktop shell for DOS, built with
|
||||
DJGPP/DPMI. DVX combines a Motif-style window manager, dirty-rectangle
|
||||
compositor, cooperative task switcher, and DXE3 dynamic application
|
||||
loading into a multitasking desktop environment where applications are
|
||||
`.app` shared libraries loaded at runtime.
|
||||
|
||||
## Components
|
||||
Targets real and emulated 486+ hardware with VESA VBE 2.0+ linear
|
||||
framebuffer. No bank switching -- LFB or fail.
|
||||
|
||||
```
|
||||
dvxgui/
|
||||
dvx/ GUI compositor library -> lib/libdvx.a
|
||||
tasks/ Cooperative task switcher -> lib/libtasks.a
|
||||
dvxshell/ Desktop shell -> bin/dvx.exe
|
||||
apps/ DXE app modules (.app files) -> bin/apps/*/*.app
|
||||
rs232/ ISR-driven UART serial driver -> lib/librs232.a
|
||||
packet/ HDLC framing, CRC, Go-Back-N -> lib/libpacket.a
|
||||
security/ DH key exchange, XTEA-CTR cipher -> lib/libsecurity.a
|
||||
seclink/ Secure serial link wrapper -> lib/libseclink.a
|
||||
proxy/ Linux SecLink-to-telnet proxy -> bin/secproxy
|
||||
termdemo/ Encrypted ANSI terminal demo -> bin/termdemo.exe
|
||||
```
|
||||
|
||||
## Building
|
||||
## Features
|
||||
|
||||
Requires the DJGPP cross-compiler (`i586-pc-msdosdjgpp-gcc`).
|
||||
- Motif/GEOS-style beveled window chrome with drag, resize, minimize,
|
||||
maximize, and restore
|
||||
- Dirty-rectangle compositor -- only changed regions are flushed to video
|
||||
memory, critical for acceptable frame rates on 486/Pentium hardware
|
||||
- 32 widget types: buttons, text inputs, list boxes, tree views, list
|
||||
views, tab controls, sliders, spinners, progress bars, dropdowns,
|
||||
combo boxes, splitters, scroll panes, ANSI terminal emulator, and more
|
||||
- Flexbox-style automatic layout engine (VBox/HBox containers with
|
||||
weighted space distribution)
|
||||
- Dropdown menus with cascading submenus, checkbox and radio items,
|
||||
keyboard accelerators, and context menus
|
||||
- Modal dialogs: message box (OK/Cancel/Yes/No/Retry) and file
|
||||
open/save with directory navigation and filter dropdown
|
||||
- 20-color theme system with live preview and INI-based theme files
|
||||
- Wallpaper support: stretch, tile, or center with bilinear scaling and
|
||||
16bpp ordered dithering
|
||||
- Live video mode switching without restart
|
||||
- Mouse wheel support (CuteMouse Wheel API)
|
||||
- Screenshots (full screen or per-window) saved as PNG
|
||||
- Clipboard (copy/cut/paste within DVX)
|
||||
- Timer widget for periodic callbacks
|
||||
- Cooperative task switcher for apps that need their own main loop
|
||||
- DXE3 dynamic application loading with crash recovery (SIGSEGV,
|
||||
SIGFPE, SIGILL caught and isolated per-app)
|
||||
- INI-based preferences system with typed read/write accessors
|
||||
- Encrypted serial networking stack (DH key exchange, XTEA-CTR cipher)
|
||||
- Platform abstraction layer -- DOS/DJGPP production target, Linux/SDL2
|
||||
development target
|
||||
|
||||
```
|
||||
make -C dvx # builds lib/libdvx.a
|
||||
make -C tasks # builds lib/libtasks.a
|
||||
make -C dvxshell # builds bin/dvx.exe
|
||||
make -C apps # builds bin/apps/*/*.app
|
||||
```
|
||||
|
||||
Set `DJGPP_PREFIX` in the Makefiles if your toolchain is installed somewhere
|
||||
other than `~/djgpp/djgpp`.
|
||||
## Target Hardware
|
||||
|
||||
- **CPU**: 486 baseline, Pentium-optimized paths where significant
|
||||
- **Video**: VESA VBE 2.0+ with linear framebuffer
|
||||
- **OS**: DOS with DPMI (CWSDPMI or equivalent)
|
||||
- **Supported depths**: 8, 15, 16, 24, 32 bpp
|
||||
- **Test platform**: 86Box with PCI video cards
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
The shell runs as a single DOS executable (`dvx.exe`) that loads
|
||||
applications dynamically via DJGPP's DXE3 shared library system. Each app
|
||||
is a `.app` file exporting an `appDescriptor` and `appMain` entry point.
|
||||
applications dynamically via DJGPP's DXE3 shared library system.
|
||||
|
||||
```
|
||||
+-------------------------------------------------------------------+
|
||||
| dvx.exe (Task 0) |
|
||||
| +-------------+ +-----------+ +----------+ +----------------+ |
|
||||
| +-------------+ +-----------+ +------------+ |
|
||||
| | shellMain | | shellApp | | shellExport| |
|
||||
| | (event loop)| | (lifecycle| | (DXE symbol| |
|
||||
|
|
@ -65,48 +79,189 @@ is a `.app` file exporting an `appDescriptor` and `appMain` entry point.
|
|||
|
||||
### App Types
|
||||
|
||||
**Callback-only** (`hasMainLoop = false`): `appMain` creates windows, registers
|
||||
callbacks, and returns. The app lives through event callbacks in the shell's
|
||||
main loop. No dedicated task or stack needed.
|
||||
**Callback-only** (`hasMainLoop = false`): `appMain` creates windows,
|
||||
registers callbacks, and returns. The app lives through event callbacks
|
||||
in the shell's main loop. No dedicated task or stack needed.
|
||||
|
||||
**Main-loop** (`hasMainLoop = true`): A cooperative task is created for the app.
|
||||
`appMain` runs its own loop calling `tsYield()` to share CPU. Used for apps that
|
||||
need continuous processing (clocks, terminal emulators, games).
|
||||
**Main-loop** (`hasMainLoop = true`): A cooperative task is created for
|
||||
the app. `appMain` runs its own loop calling `tsYield()` to share CPU.
|
||||
Used for apps that need continuous processing (clocks, terminal
|
||||
emulators, games).
|
||||
|
||||
### Crash Recovery
|
||||
|
||||
The shell installs signal handlers for SIGSEGV, SIGFPE, and SIGILL. If an app
|
||||
crashes, the handler `longjmp`s back to the shell's main loop, the crashed app
|
||||
is force-killed, and the shell continues running. Diagnostic information
|
||||
(registers, faulting EIP) is logged to `dvx.log`.
|
||||
The shell installs signal handlers for SIGSEGV, SIGFPE, and SIGILL. If
|
||||
an app crashes, the handler `longjmp`s back to the shell's main loop,
|
||||
the crashed app is force-killed, and the shell continues running.
|
||||
Diagnostic information (registers, faulting EIP) is logged to `dvx.log`.
|
||||
|
||||
## Sample Apps
|
||||
|
||||
| App | Type | Description |
|
||||
|-----|------|-------------|
|
||||
| `progman.app` | Callback | Program Manager: app launcher grid, Task Manager |
|
||||
| `notepad.app` | Callback | Text editor with file I/O |
|
||||
| `clock.app` | Main-loop | Digital clock (multi-instance capable) |
|
||||
| `dvxdemo.app` | Callback | Widget system showcase |
|
||||
## Directory Structure
|
||||
|
||||
## Target Platform
|
||||
```
|
||||
dvxgui/
|
||||
dvx/ GUI compositor library (libdvx.a)
|
||||
platform/ Platform abstraction (DOS/DJGPP, Linux/SDL2)
|
||||
widgets/ Widget system (32 types, one file per type)
|
||||
thirdparty/ stb_image.h, stb_image_write.h
|
||||
tasks/ Cooperative task switcher (libtasks.a)
|
||||
thirdparty/ stb_ds.h
|
||||
dvxshell/ Desktop shell (dvx.exe)
|
||||
apps/ DXE app modules (.app files)
|
||||
progman/ Program Manager -- app launcher grid
|
||||
notepad/ Text editor with file I/O
|
||||
clock/ Digital clock (multi-instance, main-loop)
|
||||
dvxdemo/ Widget system showcase / demo app
|
||||
cpanel/ Control Panel -- themes, wallpaper, video, mouse
|
||||
imgview/ Image Viewer -- BMP/PNG/JPEG/GIF display
|
||||
rs232/ ISR-driven UART serial driver (librs232.a)
|
||||
packet/ HDLC framing, CRC-16, Go-Back-N (libpacket.a)
|
||||
security/ DH key exchange, XTEA-CTR cipher (libsecurity.a)
|
||||
seclink/ Secure serial link wrapper (libseclink.a)
|
||||
proxy/ Linux SecLink-to-telnet proxy (secproxy)
|
||||
termdemo/ Encrypted ANSI terminal demo (termdemo.exe)
|
||||
themes/ Color theme files (.thm)
|
||||
wpaper/ Bundled wallpaper images
|
||||
bin/ Build output (dvx.exe, apps/, config/)
|
||||
lib/ Build output (static libraries)
|
||||
releases/ Release archives
|
||||
```
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
Requires the DJGPP cross-compiler (`i586-pc-msdosdjgpp-gcc`).
|
||||
|
||||
```bash
|
||||
# Build everything (dvx lib, tasks lib, shell, all apps)
|
||||
make
|
||||
|
||||
# Build individual components
|
||||
make -C dvx # builds lib/libdvx.a
|
||||
make -C tasks # builds lib/libtasks.a
|
||||
make -C dvxshell # builds bin/dvx.exe
|
||||
make -C apps # builds bin/apps/*/*.app
|
||||
|
||||
# Clean all build artifacts
|
||||
make clean
|
||||
```
|
||||
|
||||
Set `DJGPP_PREFIX` in the component Makefiles if your toolchain is
|
||||
installed somewhere other than `~/djgpp/djgpp`.
|
||||
|
||||
- **CPU**: 486 baseline, Pentium-optimized paths where significant
|
||||
- **Video**: VESA VBE 2.0+ with linear framebuffer
|
||||
- **OS**: DOS with DPMI (CWSDPMI or equivalent)
|
||||
- **Test platform**: 86Box
|
||||
|
||||
## Deployment
|
||||
|
||||
### CD-ROM ISO (86Box)
|
||||
|
||||
The primary deployment method is an ISO image mounted as a CD-ROM in
|
||||
86Box:
|
||||
|
||||
```bash
|
||||
./mkcd.sh
|
||||
```
|
||||
|
||||
This builds everything, then creates an ISO 9660 image with 8.3
|
||||
filenames at `~/.var/app/net._86box._86Box/data/86Box/dvx.iso`. Mount
|
||||
it in 86Box as a CD-ROM drive and run `D:\DVX.EXE` (or whatever drive
|
||||
letter is assigned).
|
||||
|
||||
### Floppy Image
|
||||
|
||||
For boot floppy setups with CD-ROM drivers:
|
||||
|
||||
```bash
|
||||
mcopy -o -i <floppy.img> bin/dvx.exe ::DVX.EXE
|
||||
mcopy -s -o -i <floppy.img> bin/apps ::APPS
|
||||
mcopy -s -o -i <floppy.img> bin/config ::CONFIG
|
||||
```
|
||||
|
||||
The `apps/` directory structure must be preserved on the target -- Program Manager
|
||||
recursively scans `apps/` for `.app` files at startup.
|
||||
The `apps/` and `config/` directory structures must be preserved on the
|
||||
target -- Program Manager recursively scans `apps/` for `.app` files at
|
||||
startup.
|
||||
|
||||
## Documentation
|
||||
|
||||
## Configuration
|
||||
|
||||
DVX reads its configuration from `CONFIG\DVX.INI` on the target
|
||||
filesystem. The INI file uses a standard `[section]` / `key = value`
|
||||
format. Settings are applied at startup and can be changed live from the
|
||||
Control Panel app.
|
||||
|
||||
```ini
|
||||
[video]
|
||||
width = 640
|
||||
height = 480
|
||||
bpp = 16
|
||||
|
||||
[mouse]
|
||||
wheel = normal
|
||||
doubleclick = 500
|
||||
acceleration = medium
|
||||
|
||||
[desktop]
|
||||
wallpaper = C:\DVX\WPAPER\SWOOP.JPG
|
||||
wallpaperMode = stretch
|
||||
theme = C:\DVX\THEMES\WIN31.THM
|
||||
```
|
||||
|
||||
### Video Section
|
||||
|
||||
- `width`, `height` -- requested resolution (closest VESA mode is used)
|
||||
- `bpp` -- preferred color depth (8, 15, 16, 24, or 32)
|
||||
|
||||
### Mouse Section
|
||||
|
||||
- `wheel` -- `normal` or `reversed`
|
||||
- `doubleclick` -- double-click speed in milliseconds (200--900)
|
||||
- `acceleration` -- `off`, `low`, `medium`, or `high`
|
||||
|
||||
### Desktop Section
|
||||
|
||||
- `wallpaper` -- path to wallpaper image (BMP, PNG, JPEG, GIF)
|
||||
- `wallpaperMode` -- `stretch`, `tile`, or `center`
|
||||
- `theme` -- path to color theme file (.thm)
|
||||
|
||||
|
||||
## Bundled Applications
|
||||
|
||||
| App | File | Type | Description |
|
||||
|-----|------|------|-------------|
|
||||
| Program Manager | `progman.app` | Callback | App launcher grid with icons; also provides the Task Manager (Ctrl+Esc) for switching between running apps |
|
||||
| Notepad | `notepad.app` | Callback | Text editor with File/Edit menus, open/save dialogs, clipboard, and undo |
|
||||
| Clock | `clock.app` | Main-loop | Digital clock display; multi-instance capable |
|
||||
| DVX Demo | `dvxdemo.app` | Callback | Widget system showcase demonstrating all 32 widget types |
|
||||
| Control Panel | `cpanel.app` | Callback | System settings: color themes with live preview, wallpaper selection, video mode switching, mouse configuration |
|
||||
| Image Viewer | `imgview.app` | Callback | Displays BMP, PNG, JPEG, and GIF images with file dialog |
|
||||
|
||||
|
||||
## Serial / Networking Stack
|
||||
|
||||
A layered encrypted serial communications stack for connecting DVX to
|
||||
remote systems (BBS, etc.) through 86Box's emulated UART:
|
||||
|
||||
| Layer | Library | Description |
|
||||
|-------|---------|-------------|
|
||||
| rs232 | `librs232.a` | ISR-driven UART driver with FIFO support, automatic UART type detection (8250 through 16750), configurable baud rate |
|
||||
| packet | `libpacket.a` | HDLC framing with byte stuffing, CRC-16 integrity checks, Go-Back-N sliding window for reliable delivery, 255-byte max payload |
|
||||
| security | `libsecurity.a` | 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA-CTR stream cipher, XTEA-CTR DRBG random number generator |
|
||||
| seclink | `libseclink.a` | Convenience wrapper: multiplexed channels (0--127), per-packet encryption flag, bulk send helper |
|
||||
| proxy | `secproxy` | Linux-side bridge: 86Box emulated serial port <-> secLink <-> telnet BBS; socket shim replaces rs232 API |
|
||||
|
||||
|
||||
## Third-Party Dependencies
|
||||
|
||||
All third-party code is vendored as single-header libraries with no
|
||||
external dependencies:
|
||||
|
||||
| Library | Location | Purpose |
|
||||
|---------|----------|---------|
|
||||
| stb_image.h | `dvx/thirdparty/` | Image loading (BMP, PNG, JPEG, GIF) |
|
||||
| stb_image_write.h | `dvx/thirdparty/` | Image writing (PNG export for screenshots) |
|
||||
| stb_ds.h | `tasks/thirdparty/` | Dynamic array and hash map (used by task manager) |
|
||||
|
||||
|
||||
## Component Documentation
|
||||
|
||||
Each component directory has its own README with detailed API reference:
|
||||
|
||||
|
|
|
|||
340
apps/README.md
340
apps/README.md
|
|
@ -1,9 +1,51 @@
|
|||
# DVX Shell Applications
|
||||
|
||||
DXE3 shared library applications loaded at runtime by the DVX Shell. Each app
|
||||
is a `.app` file (DXE3 format) placed under the `apps/` directory tree. The
|
||||
Program Manager scans this directory recursively and displays all discovered
|
||||
apps.
|
||||
DXE3 application modules for the DVX Shell. Each app is a `.app` file (DXE3
|
||||
shared object format) placed in a subdirectory under `apps/`. The Program
|
||||
Manager scans this directory recursively at startup and displays all discovered
|
||||
apps in a launcher grid.
|
||||
|
||||
## App Contract
|
||||
|
||||
Every DXE app must export two symbols and may optionally export a third:
|
||||
|
||||
```c
|
||||
// Required: app metadata
|
||||
AppDescriptorT appDescriptor = {
|
||||
.name = "My App",
|
||||
.hasMainLoop = false,
|
||||
.multiInstance = false,
|
||||
.stackSize = SHELL_STACK_DEFAULT,
|
||||
.priority = TS_PRIORITY_NORMAL
|
||||
};
|
||||
|
||||
// Required: entry point -- called once by the shell after dlopen
|
||||
int32_t appMain(DxeAppContextT *ctx);
|
||||
|
||||
// Optional: graceful shutdown hook -- called before force-kill
|
||||
void appShutdown(void);
|
||||
```
|
||||
|
||||
`appMain` receives a `DxeAppContextT` with:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `shellCtx` | `AppContextT *` | The shell's GUI context -- pass to all `dvx*`/`wgt*` calls |
|
||||
| `appId` | `int32_t` | This app's unique ID (1-based slot index; 0 = shell) |
|
||||
| `appDir` | `char[260]` | Directory containing the `.app` file (for relative resource paths) |
|
||||
| `configDir` | `char[260]` | Writable config directory (`CONFIG/<apppath>/`) |
|
||||
|
||||
Return 0 from `appMain` on success, non-zero on failure (shell will unload).
|
||||
|
||||
### AppDescriptorT Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | `char[64]` | Display name shown in Task Manager and title bars |
|
||||
| `hasMainLoop` | `bool` | `false` = callback-only (runs in task 0); `true` = gets own cooperative task |
|
||||
| `multiInstance` | `bool` | `true` = allow multiple instances via temp file copy |
|
||||
| `stackSize` | `int32_t` | Task stack in bytes; `SHELL_STACK_DEFAULT` (0) for the default |
|
||||
| `priority` | `int32_t` | `TS_PRIORITY_LOW`, `TS_PRIORITY_NORMAL`, or `TS_PRIORITY_HIGH` |
|
||||
|
||||
## Building
|
||||
|
||||
|
|
@ -12,17 +54,237 @@ make # builds all .app files into ../bin/apps/<name>/
|
|||
make clean # removes objects and binaries
|
||||
```
|
||||
|
||||
Requires `lib/libdvx.a`, `lib/libtasks.a`, and the DXE3 tools (`dxe3gen`)
|
||||
from the DJGPP toolchain.
|
||||
Each app is compiled to an object file with the DJGPP cross-compiler, then
|
||||
packaged into a `.app` via `dxe3gen`:
|
||||
|
||||
## Applications
|
||||
```makefile
|
||||
$(BINDIR)/myapp/myapp.app: $(OBJDIR)/myapp.o | $(BINDIR)/myapp
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
```
|
||||
|
||||
| App | File | Type | Description |
|
||||
|-----|------|------|-------------|
|
||||
| Program Manager | `progman/progman.c` | Callback | Desktop app: app launcher grid, Task Manager (Ctrl+Esc), window management |
|
||||
| Notepad | `notepad/notepad.c` | Callback | Text editor with file I/O, dirty tracking via hash |
|
||||
| Clock | `clock/clock.c` | Main-loop | Digital clock, multi-instance capable |
|
||||
| DVX Demo | `dvxdemo/dvxdemo.c` | Callback | Widget system showcase with all widget types |
|
||||
- `-E _appDescriptor` and `-E _appMain` export the required symbols (COFF
|
||||
underscore prefix).
|
||||
- `-E _appShutdown` is added for apps that export a shutdown hook (e.g.,
|
||||
Clock).
|
||||
- `-U` marks all other symbols as unresolved imports to be resolved from the
|
||||
shell's export table at dlopen time.
|
||||
|
||||
Requires `lib/libdvx.a`, `lib/libtasks.a`, and the DXE3 tools from the DJGPP
|
||||
toolchain.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
apps/
|
||||
Makefile -- build rules for all apps
|
||||
README.md -- this file
|
||||
progman/
|
||||
progman.c -- Program Manager (desktop shell)
|
||||
notepad/
|
||||
notepad.c -- text editor
|
||||
clock/
|
||||
clock.c -- digital clock
|
||||
cpanel/
|
||||
cpanel.c -- Control Panel (system settings)
|
||||
imgview/
|
||||
imgview.c -- image viewer
|
||||
dvxdemo/
|
||||
dvxdemo.c -- widget showcase
|
||||
*.bmp -- toolbar icons and sample images
|
||||
```
|
||||
|
||||
Each app lives in its own subdirectory. The subdirectory name becomes the
|
||||
output path under `bin/apps/` (e.g., `bin/apps/progman/progman.app`).
|
||||
|
||||
## Bundled Applications
|
||||
|
||||
### Program Manager (`progman/`)
|
||||
|
||||
The desktop shell and default app launcher. Loaded automatically by the shell
|
||||
at startup as the "desktop app" -- closing it prompts to exit the entire DVX
|
||||
Shell.
|
||||
|
||||
- **App launcher grid**: scans `apps/` recursively for `.app` files (skipping
|
||||
itself), displays them as a grid of buttons. Click or double-click to
|
||||
launch.
|
||||
- **Menu bar**: File (Run..., Exit Shell), Options (Minimize on Run), Window
|
||||
(Cascade, Tile, Tile H, Tile V), Help (About, System Information, Task
|
||||
Manager).
|
||||
- **Minimize on Run**: optional preference -- when enabled, Program Manager
|
||||
minimizes itself after launching an app, getting out of the way.
|
||||
- **Status bar**: shows the count of running applications, updated in
|
||||
real-time via `shellRegisterDesktopUpdate()`.
|
||||
- **System Information**: opens a read-only text area showing CPU, memory,
|
||||
drive, and video details gathered by the platform layer.
|
||||
- **Preferences**: saved to `CONFIG/PROGMAN/progman.ini` via the standard
|
||||
prefs system. Currently stores the "Minimize on Run" setting.
|
||||
|
||||
Type: callback-only. Single instance.
|
||||
|
||||
### Notepad (`notepad/`)
|
||||
|
||||
A basic text editor with file I/O and dirty-change tracking.
|
||||
|
||||
- **TextArea widget**: handles all editing -- keyboard input, cursor movement,
|
||||
selection, scrolling, word wrap, copy/paste, undo (Ctrl+Z). Notepad only
|
||||
wires up menus and file I/O around it.
|
||||
- **File menu**: New, Open, Save, Save As, Exit.
|
||||
- **Edit menu**: Cut, Copy, Paste, Select All (Ctrl+X/C/V/A).
|
||||
- **CR/LF handling**: files are opened in binary mode to avoid DJGPP's
|
||||
translation. `platformStripLineEndings()` normalizes on load;
|
||||
`platformLineEnding()` writes platform-native line endings on save.
|
||||
- **Dirty tracking**: uses djb2-xor hash of the text content. Cheap detection
|
||||
without storing a full shadow buffer. Prompts "Save changes?" on close/new/
|
||||
open if dirty.
|
||||
- **32 KB text buffer**: keeps memory bounded on DOS. Larger files are
|
||||
silently truncated on load.
|
||||
- **Multi-instance**: each instance gets its own DXE code+data via temp file
|
||||
copy. Window positions are offset +20px so instances cascade naturally.
|
||||
|
||||
Type: callback-only. Multi-instance.
|
||||
|
||||
### Clock (`clock/`)
|
||||
|
||||
A digital clock displaying 12-hour time and date, centered in a small
|
||||
non-resizable window.
|
||||
|
||||
- **Main-loop app**: polls `time()` each iteration, repaints when the second
|
||||
changes, then calls `tsYield()`. CPU usage is near zero because the check
|
||||
is cheap and yields immediately when nothing changes.
|
||||
- **Raw paint callback**: renders directly into the window's content buffer
|
||||
using `rectFill` and `drawText` -- no widget tree. Demonstrates the
|
||||
lower-level alternative to the widget system for custom rendering.
|
||||
- **Shutdown hook**: exports `appShutdown()` so the shell can signal a clean
|
||||
exit when force-killing via Task Manager or during shell shutdown.
|
||||
- **Low priority**: uses `TS_PRIORITY_LOW` since clock updates are cosmetic
|
||||
and should never preempt interactive apps.
|
||||
|
||||
Type: main-loop. Multi-instance.
|
||||
|
||||
### Control Panel (`cpanel/`)
|
||||
|
||||
System configuration with four tabs, all changes previewing live. OK saves to
|
||||
`CONFIG/DVX.INI`; Cancel reverts to the state captured when the panel opened.
|
||||
|
||||
**Mouse tab:**
|
||||
- Scroll wheel direction (Normal / Reversed) via dropdown.
|
||||
- Double-click speed (200-900 ms) via slider with numeric label and test
|
||||
button.
|
||||
- Mouse acceleration (Off / Low / Medium / High) via dropdown.
|
||||
- All changes apply immediately via `dvxSetMouseConfig()`.
|
||||
|
||||
**Colors tab:**
|
||||
- List of all 20 system colors (`ColorCountE` entries from `dvxColorName`).
|
||||
- RGB sliders (0-255) for the selected color, with numeric labels and a
|
||||
canvas swatch preview.
|
||||
- Changes apply live via `dvxSetColor()` -- the entire desktop updates in
|
||||
real time.
|
||||
- **Themes**: dropdown of `.thm` files from `CONFIG/THEMES/`, with Apply,
|
||||
Load..., Save As..., and Reset buttons. Themes are loaded/saved via
|
||||
`dvxLoadTheme()`/`dvxSaveTheme()`.
|
||||
- Reset restores the compiled-in default color scheme.
|
||||
|
||||
**Desktop tab:**
|
||||
- Wallpaper list: scans `CONFIG/WPAPER/` for BMP/JPG/PNG files.
|
||||
- Apply, Browse..., and Clear buttons.
|
||||
- Mode dropdown: Stretch, Tile, Center. Changes apply live via
|
||||
`dvxSetWallpaperMode()`.
|
||||
|
||||
**Video tab:**
|
||||
- List of all enumerated VESA modes with human-readable depth names (e.g.,
|
||||
"800x600 65 thousand colors").
|
||||
- Apply Mode button or double-click to switch.
|
||||
- **10-second confirmation dialog**: after switching, a modal "Keep this
|
||||
mode?" dialog counts down. If the user clicks Yes, the mode is kept.
|
||||
Clicking No, closing the dialog, or letting the timer expire reverts to
|
||||
the previous mode. Prevents being stuck in an unsupported mode.
|
||||
|
||||
Type: callback-only. Single instance.
|
||||
|
||||
### Image Viewer (`imgview/`)
|
||||
|
||||
Displays BMP, PNG, JPG, and GIF images loaded via stb_image.
|
||||
|
||||
- **Bilinear scaling**: images are scaled to fit the window while preserving
|
||||
aspect ratio. The scaler converts from RGB to the native pixel format
|
||||
(8/16/32 bpp) during the scale pass.
|
||||
- **Deferred resize**: during a window drag-resize, the old scaled image is
|
||||
shown. The expensive bilinear rescale only runs after the drag ends
|
||||
(`sAc->stack.resizeWindow < 0`), avoiding per-frame scaling lag.
|
||||
- **Responsive scaling**: for large images, `dvxUpdate()` is called every 32
|
||||
scanlines during the scale loop to keep the UI responsive.
|
||||
- **File menu**: Open (Ctrl+O), Close. Keyboard accelerator table registered.
|
||||
- **Multi-instance**: multiple viewers can be open simultaneously, each with
|
||||
its own image.
|
||||
- **Raw paint callback**: renders directly into the content buffer with a dark
|
||||
gray background and centered blit of the scaled image.
|
||||
|
||||
Type: callback-only. Multi-instance.
|
||||
|
||||
### DVX Demo (`dvxdemo/`)
|
||||
|
||||
A comprehensive widget showcase and integration test. Opens several windows
|
||||
demonstrating the full DVX widget system:
|
||||
|
||||
- **Main window**: three raw-paint windows -- text rendering with full menu
|
||||
bar/accelerators/context menu, vertical gradient, and checkerboard pattern
|
||||
with scrollbars.
|
||||
- **Widget Demo window**: form pattern with labeled inputs (text, password,
|
||||
masked phone number), checkboxes, radio groups, single and multi-select
|
||||
list boxes with context menus and drag reorder.
|
||||
- **Advanced Widgets window**: nine tab pages covering every widget type --
|
||||
dropdown, combo box, progress bar (horizontal and vertical), slider,
|
||||
spinner, tree view (with drag reorder), multi-column list view (with
|
||||
multi-select and drag reorder), scroll pane, toolbar (with image buttons
|
||||
and text fallback), image from file, text area, canvas (with mouse
|
||||
drawing), splitter (nested horizontal+vertical for explorer-style layout),
|
||||
and a disabled-state comparison of all widget types.
|
||||
- **ANSI Terminal window**: terminal emulator widget with sample output
|
||||
demonstrating bold, reverse, blink, all 16 colors, background colors,
|
||||
CP437 box-drawing characters, and 500-line scrollback.
|
||||
|
||||
Type: callback-only. Single instance.
|
||||
|
||||
## App Preferences
|
||||
|
||||
Apps that need persistent settings use the shell's config directory system:
|
||||
|
||||
```c
|
||||
// In appMain:
|
||||
shellEnsureConfigDir(ctx); // create CONFIG/<apppath>/ if needed
|
||||
|
||||
char path[260];
|
||||
shellConfigPath(ctx, "settings.ini", path, sizeof(path));
|
||||
prefsLoad(path);
|
||||
|
||||
// Read/write:
|
||||
int32_t val = prefsGetInt("section", "key", defaultVal);
|
||||
prefsSetInt("section", "key", newVal);
|
||||
prefsSave();
|
||||
```
|
||||
|
||||
The preferences system handles INI file format with `[section]` headers and
|
||||
`key=value` pairs. Missing files or keys silently return defaults.
|
||||
|
||||
## Event Model
|
||||
|
||||
DVX apps receive events through two mechanisms:
|
||||
|
||||
**Widget callbacks** (high-level):
|
||||
- `onClick`, `onDblClick`, `onChange` on individual widgets.
|
||||
- The widget system handles focus, tab order, mouse hit testing, keyboard
|
||||
dispatch, and repainting automatically.
|
||||
- Used by most apps for standard UI (buttons, inputs, lists, sliders, etc.).
|
||||
|
||||
**Window callbacks** (low-level):
|
||||
- `onPaint(win, dirtyRect)` -- render directly into the window's content
|
||||
buffer. Used by Clock, Image Viewer, and the DVX Demo paint windows.
|
||||
- `onClose(win)` -- window close requested (close gadget, Alt+F4).
|
||||
- `onResize(win, contentW, contentH)` -- window was resized.
|
||||
- `onMenu(win, menuId)` -- menu item selected or keyboard accelerator fired.
|
||||
|
||||
Both mechanisms can be mixed in the same app. For example, DVX Demo uses
|
||||
widgets in some windows and raw paint callbacks in others.
|
||||
|
||||
## Writing a New App
|
||||
|
||||
|
|
@ -86,6 +348,11 @@ AppDescriptorT appDescriptor = {
|
|||
static bool sQuit = false;
|
||||
|
||||
static void onClose(WindowT *win) {
|
||||
(void)win;
|
||||
sQuit = true;
|
||||
}
|
||||
|
||||
void appShutdown(void) {
|
||||
sQuit = true;
|
||||
}
|
||||
|
||||
|
|
@ -111,16 +378,25 @@ int32_t appMain(DxeAppContextT *ctx) {
|
|||
|
||||
### Adding to the build
|
||||
|
||||
Add your app directory and source to `apps/Makefile`. Each app is compiled to
|
||||
an object file, then linked into a `.app` via `dxe3gen`:
|
||||
Add your app directory and source to `apps/Makefile`:
|
||||
|
||||
```makefile
|
||||
$(BIN_DIR)/myapp.app: $(OBJ_DIR)/myapp/myapp.o
|
||||
APPS = ... myapp
|
||||
|
||||
myapp: $(BINDIR)/myapp/myapp.app
|
||||
|
||||
$(BINDIR)/myapp/myapp.app: $(OBJDIR)/myapp.o | $(BINDIR)/myapp
|
||||
$(DXE3GEN) -o $@ -E _appDescriptor -E _appMain -U $<
|
||||
|
||||
$(OBJDIR)/myapp.o: myapp/myapp.c | $(OBJDIR)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(BINDIR)/myapp:
|
||||
mkdir -p $(BINDIR)/myapp
|
||||
```
|
||||
|
||||
The `-E` flags export the required symbols. `-U` marks unresolved symbols as
|
||||
imports to be resolved from the shell's export table at load time.
|
||||
Add `-E _appShutdown` to the `dxe3gen` line if the app exports a shutdown
|
||||
hook.
|
||||
|
||||
## App Guidelines
|
||||
|
||||
|
|
@ -128,14 +404,22 @@ imports to be resolved from the shell's export table at load time.
|
|||
`SHELL_STACK_DEFAULT`.
|
||||
- Use `ctx->shellCtx` (the `AppContextT *`) for all DVX API calls.
|
||||
- Callback-only apps must destroy their own windows in `onClose` via
|
||||
`dvxDestroyWindow()`. The shell detects the last window closing and
|
||||
reaps the app.
|
||||
`dvxDestroyWindow()`. The shell detects the last window closing and reaps
|
||||
the app automatically.
|
||||
- Main-loop apps must call `tsYield()` regularly. A task that never yields
|
||||
blocks the entire system.
|
||||
- Use file-scoped `static` variables for app state. Each DXE has its own data
|
||||
segment, so there is no collision between apps.
|
||||
- Set `multiInstance = true` in the descriptor if the app can safely run
|
||||
multiple copies simultaneously.
|
||||
- Avoid `static inline` functions in shared headers. Code inlined into the DXE
|
||||
binary cannot be updated without recompiling the app. Use macros for trivial
|
||||
expressions or regular functions exported through the shell's DXE table.
|
||||
blocks the entire system (cooperative multitasking -- no preemption).
|
||||
- Export `appShutdown()` for main-loop apps so the shell can signal a clean
|
||||
exit on force-kill or shell shutdown.
|
||||
- Use file-scoped `static` variables for app state. Each DXE has its own
|
||||
data segment, so there is no collision between apps even with identical
|
||||
variable names.
|
||||
- Set `multiInstance = true` only if the app can safely run multiple copies
|
||||
simultaneously. Each instance gets independent globals and statics.
|
||||
- Avoid `static inline` functions in shared headers. Code inlined into the
|
||||
DXE binary cannot be updated without recompiling the app. Use macros for
|
||||
trivial expressions or regular functions exported through the shell's DXE
|
||||
table.
|
||||
- Use `ctx->appDir` for loading app-relative resources (icons, data files).
|
||||
The working directory is shared by all apps and belongs to the shell.
|
||||
- Use `shellEnsureConfigDir()` + `shellConfigPath()` for persistent settings.
|
||||
Never write to the app's own directory -- use `CONFIG/<apppath>/` instead.
|
||||
|
|
|
|||
|
|
@ -878,7 +878,9 @@ static void restoreSnapshot(void) {
|
|||
dvxChangeVideoMode(sAc, sSavedVideoW, sSavedVideoH, sSavedVideoBpp);
|
||||
}
|
||||
|
||||
// Restore wallpaper mode and image
|
||||
// Restore wallpaper only if path or mode changed
|
||||
if (strcmp(sAc->wallpaperPath, sSavedWallpaperPath) != 0 ||
|
||||
sAc->wallpaperMode != sSavedWpMode) {
|
||||
sAc->wallpaperMode = sSavedWpMode;
|
||||
|
||||
if (sSavedWallpaperPath[0]) {
|
||||
|
|
@ -887,6 +889,7 @@ static void restoreSnapshot(void) {
|
|||
dvxSetWallpaper(sAc, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ============================================================
|
||||
|
|
|
|||
2024
dvx/README.md
2024
dvx/README.md
File diff suppressed because it is too large
Load diff
|
|
@ -1,140 +1,362 @@
|
|||
# DVX Shell
|
||||
|
||||
Windows 3.x-style desktop shell for DOS. Loads applications as DXE3 shared
|
||||
libraries and includes crash
|
||||
recovery so one bad app doesn't take down the system.
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
make # builds ../bin/dvx.exe
|
||||
make clean # removes objects and binary
|
||||
```
|
||||
|
||||
Requires `lib/libdvx.a` and `lib/libtasks.a` to be built first.
|
||||
The DVX Shell (`dvx.exe`) is the host process for the DVX desktop environment.
|
||||
It initializes the GUI subsystem, loads DXE3 application modules at runtime,
|
||||
runs the cooperative main loop, and provides crash recovery so a faulting app
|
||||
does not take down the entire system. Think of it as the Windows 3.1 Program
|
||||
Manager and kernel combined into one executable.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `shellMain.c` | Entry point, main loop, crash recovery, logging |
|
||||
| `shellApp.c` | App loading (dlopen), lifecycle, reaping, resource tracking |
|
||||
| `shellApp.h` | ShellAppT, AppDescriptorT, AppStateE, DxeAppContextT, shell API |
|
||||
| `shellExport.c` | DXE export table and wrapper functions |
|
||||
| `Makefile` | Build rules, links `-ldvx -ltasks -ldxe -lm` |
|
||||
| `shellMain.c` | Entry point, main loop, crash recovery, logging, desktop update callbacks |
|
||||
| `shellApp.c` | App loading (dlopen/dlsym), lifecycle state machine, reaping, resource tracking, config dirs |
|
||||
| `shellApp.h` | `ShellAppT`, `AppDescriptorT`, `AppStateE`, `DxeAppContextT`, public shell API |
|
||||
| `shellExport.c` | DXE export table (400+ symbols), wrapper functions for resource tracking |
|
||||
| `shellInfo.c` | System information gathering (delegates to platform layer), caches result |
|
||||
| `shellInfo.h` | `shellInfoInit()`, `shellGetSystemInfo()` |
|
||||
| `shellTaskMgr.c` | Task Manager window -- list view, Switch To / End Task / Run buttons |
|
||||
| `shellTaskMgr.h` | `shellTaskMgrOpen()`, `shellTaskMgrRefresh()` |
|
||||
| `Makefile` | Cross-compile rules, links `-ldvx -ltasks -lm` |
|
||||
|
||||
## Shell Main Loop
|
||||
## Building
|
||||
|
||||
Each iteration of the main loop:
|
||||
|
||||
1. `dvxUpdate()` -- process input events, dispatch callbacks, composite dirty rects
|
||||
2. `tsYield()` -- give CPU time to main-loop app tasks
|
||||
3. `shellReapApps()` -- clean up apps that terminated this frame
|
||||
4. `desktopUpdate()` -- notify the desktop app if anything changed
|
||||
|
||||
An idle callback (`idleYield`) yields to app tasks during quiet periods when
|
||||
there are no events or dirty rects to process.
|
||||
|
||||
## DXE App Contract
|
||||
|
||||
Every `.app` file must export these symbols:
|
||||
|
||||
```c
|
||||
// Required: app metadata
|
||||
AppDescriptorT appDescriptor = {
|
||||
.name = "My App",
|
||||
.hasMainLoop = false,
|
||||
.multiInstance = false,
|
||||
.stackSize = SHELL_STACK_DEFAULT,
|
||||
.priority = TS_PRIORITY_NORMAL
|
||||
};
|
||||
|
||||
// Required: entry point
|
||||
int32_t appMain(DxeAppContextT *ctx);
|
||||
|
||||
// Optional: graceful shutdown hook
|
||||
void appShutdown(void);
|
||||
```
|
||||
make # builds ../bin/dvx.exe (also builds libdvx.a, libtasks.a)
|
||||
make clean # removes objects and binary
|
||||
```
|
||||
|
||||
### AppDescriptorT Fields
|
||||
Requires the DJGPP cross-compiler toolchain and the DXE3 tools (`dxe3gen`).
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | `char[64]` | Display name shown in Task Manager |
|
||||
| `hasMainLoop` | `bool` | `true` = gets its own cooperative task; `false` = callback-only |
|
||||
| `multiInstance` | `bool` | `true` = allow multiple instances via temp file copy |
|
||||
| `stackSize` | `int32_t` | Task stack size (`SHELL_STACK_DEFAULT` for 8 KB default) |
|
||||
| `priority` | `int32_t` | Task priority (`TS_PRIORITY_LOW`/`NORMAL`/`HIGH`) |
|
||||
## Startup Sequence
|
||||
|
||||
### DxeAppContextT
|
||||
`main()` in `shellMain.c` performs initialization in this order:
|
||||
|
||||
Passed to `appMain()`:
|
||||
1. **Change to exe directory** -- resolve the directory containing `dvx.exe`
|
||||
via `argv[0]` and `platformChdir()` so that relative paths (`CONFIG/`,
|
||||
`APPS/`, etc.) work regardless of where the user launched from.
|
||||
2. **Truncate log** -- open `dvx.log` for write to clear it, then close.
|
||||
All subsequent writes use append-per-write (the file is never held open).
|
||||
3. **Load preferences** -- `prefsLoad("CONFIG/DVX.INI")`. Missing file or
|
||||
keys silently fall back to compiled-in defaults.
|
||||
4. **dvxInit** -- initialize VESA video (LFB), backbuffer, compositor, window
|
||||
manager, font, cursor, input subsystems. Reads video width/height/bpp from
|
||||
preferences (default 640x480x16).
|
||||
5. **Mouse config** -- read wheel direction, double-click speed, acceleration
|
||||
from `[mouse]` section and call `dvxSetMouseConfig()`.
|
||||
6. **Color scheme** -- read `[colors]` section (20 RGB triplets), apply via
|
||||
`dvxApplyColorScheme()`.
|
||||
7. **Wallpaper** -- read `[desktop]` section for wallpaper path and mode
|
||||
(stretch/tile/center), load via `dvxSetWallpaper()`.
|
||||
8. **Video mode log** -- enumerate all available VESA modes to `dvx.log`.
|
||||
9. **Task system** -- `tsInit()`, set shell task (task 0) to
|
||||
`TS_PRIORITY_HIGH` so the UI stays responsive under load.
|
||||
10. **System info** -- `shellInfoInit()` gathers CPU, memory, drive info via
|
||||
the platform layer and logs it.
|
||||
11. **DXE exports** -- `shellExportInit()` calls `dlregsym()` to register the
|
||||
export table. Must happen before any `dlopen()`.
|
||||
12. **App slot table** -- `shellAppInit()` zeroes the 32-slot fixed array.
|
||||
13. **Idle/hotkey callbacks** -- wire up `idleYield`, `ctrlEscHandler`,
|
||||
`titleChangeHandler` on the `AppContextT`.
|
||||
14. **Desktop app** -- `shellLoadApp(ctx, "apps/progman/progman.app")`. If
|
||||
this fails, the shell exits.
|
||||
15. **Crash handlers** -- `installCrashHandler()` registers signal handlers
|
||||
for SIGSEGV, SIGFPE, SIGILL. Installed last so initialization crashes
|
||||
get the default DJGPP abort-with-register-dump instead of our recovery
|
||||
path.
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `shellCtx` | `AppContextT *` | The shell's GUI context for creating windows, drawing, etc. |
|
||||
| `appId` | `int32_t` | This app's unique ID (1-based slot index) |
|
||||
| `appDir` | `char[260]` | Directory containing the `.app` file for relative resource paths |
|
||||
## Main Loop
|
||||
|
||||
## App Types
|
||||
The main loop runs until `dvxQuit()` sets `ctx->running = false`:
|
||||
|
||||
**Callback-only** (`hasMainLoop = false`):
|
||||
- `appMain` called in shell's task 0, creates windows, registers callbacks, returns 0
|
||||
- App lives through event callbacks dispatched by `dvxUpdate()`
|
||||
- Lifecycle ends when the last window is closed
|
||||
```
|
||||
while (ctx->running) {
|
||||
dvxUpdate(ctx); // (1)
|
||||
tsYield(); // (2)
|
||||
shellReapApps(ctx); // (3)
|
||||
shellDesktopUpdate(); // (4)
|
||||
}
|
||||
```
|
||||
|
||||
**Main-loop** (`hasMainLoop = true`):
|
||||
- Shell creates a cooperative task via `tsCreate()`
|
||||
- `appMain` runs in that task with its own loop calling `tsYield()`
|
||||
- Lifecycle ends when `appMain` returns
|
||||
|
||||
## Multi-Instance Support
|
||||
|
||||
DXE3's `dlopen` is reference-counted per path: loading the same `.app` twice
|
||||
returns the same handle, sharing all global/static state. For apps that support
|
||||
multiple instances (`multiInstance = true`), the shell copies the `.app` to a
|
||||
temp file before loading, giving each instance independent code and data. The
|
||||
temp file is cleaned up when the app terminates.
|
||||
|
||||
Apps that don't support multiple instances (`multiInstance = false`, the default)
|
||||
are blocked from loading a second time with an error message.
|
||||
|
||||
Temp file paths use the `TEMP` or `TMP` environment variable if set, falling
|
||||
back to the current directory.
|
||||
|
||||
## Resource Tracking
|
||||
|
||||
The shell tracks which app owns which windows via `sCurrentAppId`, a global
|
||||
set before calling any app code. The shell's `dvxCreateWindow` wrapper stamps
|
||||
`win->appId` with the current app ID. On termination, the shell destroys all
|
||||
windows belonging to the app.
|
||||
1. **dvxUpdate** -- processes mouse/keyboard input, dispatches widget and
|
||||
window callbacks, composites dirty rects, flushes to the LFB. This is the
|
||||
shell's primary job. When idle (no events, no dirty rects), dvxUpdate
|
||||
calls the registered `idleCallback` which yields to app tasks.
|
||||
2. **tsYield** -- explicit yield to give app tasks CPU time even during busy
|
||||
frames with many repaints. Without this, a flurry of mouse-move events
|
||||
could starve app tasks because dvxUpdate would keep finding work.
|
||||
3. **shellReapApps** -- scans the 32-slot app table for apps in
|
||||
`AppStateTerminatingE`. Calls the shutdown hook (if present), destroys
|
||||
all windows owned by the app, kills the task, closes the DXE handle,
|
||||
and cleans up temp files. Returns true if anything was reaped.
|
||||
4. **shellDesktopUpdate** -- if apps were reaped, iterates the registered
|
||||
desktop update callback list so listeners (Program Manager, Task Manager)
|
||||
can refresh their UI.
|
||||
|
||||
## Crash Recovery
|
||||
|
||||
Signal handlers for SIGSEGV, SIGFPE, and SIGILL `longjmp` back to the shell's
|
||||
main loop. The scheduler is fixed via `tsRecoverToMain()`, the crashed app is
|
||||
force-killed, and a diagnostic message is displayed. Register state and app
|
||||
identity are logged to `dvx.log`.
|
||||
The shell provides Windows 3.1-style fault tolerance using `setjmp`/`longjmp`:
|
||||
|
||||
1. `installCrashHandler()` registers `crashHandler` for SIGSEGV, SIGFPE,
|
||||
SIGILL.
|
||||
2. `setjmp(sCrashJmp)` in `main()` establishes the recovery point.
|
||||
3. If a signal fires (in any task), `crashHandler` logs the crash details
|
||||
(signal, app name, full register dump from `__djgpp_exception_state_ptr`),
|
||||
re-installs the handler (DJGPP uses SysV semantics -- handler resets to
|
||||
`SIG_DFL` after each delivery), then `longjmp(sCrashJmp, 1)`.
|
||||
4. `longjmp` restores the main task's stack frame to the `setjmp` point.
|
||||
This is safe because cooperative switching means the main task's stack
|
||||
is always intact -- it was cleanly suspended at a yield point.
|
||||
5. `tsRecoverToMain()` fixes the task scheduler's `currentIdx` so it points
|
||||
back to task 0 instead of the crashed task.
|
||||
6. The crashed app is force-killed via `shellForceKillApp()`, and a message
|
||||
box is displayed: "'AppName' has caused a fault and will be terminated."
|
||||
7. The desktop update callbacks are notified so the UI refreshes.
|
||||
|
||||
The register dump logged includes EIP, CS, all GPRs (EAX-EDI), segment
|
||||
registers, and EFLAGS -- invaluable for post-mortem debugging.
|
||||
|
||||
## App Lifecycle
|
||||
|
||||
### DXE3 Loading
|
||||
|
||||
DXE3 is DJGPP's dynamic linking mechanism. Each `.app` file is a DXE3 shared
|
||||
object. The shell resolves symbols from the export table registered via
|
||||
`dlregsym()`. The load sequence in `shellLoadApp()`:
|
||||
|
||||
1. Allocate a slot from the 32-entry fixed array (slot 0 is the shell).
|
||||
2. Check if this DXE path is already loaded (`findLoadedPath`).
|
||||
- If the existing app's descriptor says `multiInstance = true`, copy
|
||||
the `.app` to a unique temp file (using `TEMP`/`TMP` env var) so
|
||||
`dlopen` gets an independent code+data image.
|
||||
- If `multiInstance = false`, block with an error message.
|
||||
3. `dlopen(loadPath, RTLD_GLOBAL)` -- load the DXE.
|
||||
4. `dlsym(handle, "_appDescriptor")` -- resolve the metadata struct.
|
||||
5. `dlsym(handle, "_appMain")` -- resolve the entry point.
|
||||
6. `dlsym(handle, "_appShutdown")` -- resolve the optional shutdown hook.
|
||||
7. Fill in the `ShellAppT` slot (name, path, handle, state, etc.).
|
||||
8. Derive `appDir` (directory containing the `.app` file) and `configDir`
|
||||
(`CONFIG/<apppath>/` -- mirrors the app's directory structure under
|
||||
`CONFIG/`).
|
||||
9. Set `sCurrentAppId` to the slot index.
|
||||
10. Launch:
|
||||
- **Callback-only** (`hasMainLoop = false`): call `entryFn()` directly
|
||||
in task 0. The app creates windows, registers callbacks, and returns.
|
||||
- **Main-loop** (`hasMainLoop = true`): call `tsCreate()` to make a
|
||||
cooperative task with the descriptor's stack size and priority. The
|
||||
task wrapper sets `sCurrentAppId`, calls `entryFn()`, and marks the
|
||||
app `AppStateTerminatingE` when it returns.
|
||||
11. Reset `sCurrentAppId` to 0. Set state to `AppStateRunningE`. Notify
|
||||
desktop update callbacks.
|
||||
|
||||
### AppDescriptorT
|
||||
|
||||
Every DXE app exports a global `AppDescriptorT`:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | `char[64]` | Display name (Task Manager, title bars) |
|
||||
| `hasMainLoop` | `bool` | `true` = dedicated cooperative task; `false` = callback-only |
|
||||
| `multiInstance` | `bool` | `true` = allow multiple simultaneous instances via temp file copy |
|
||||
| `stackSize` | `int32_t` | Task stack in bytes (`SHELL_STACK_DEFAULT` = use default) |
|
||||
| `priority` | `int32_t` | `TS_PRIORITY_LOW` / `TS_PRIORITY_NORMAL` / `TS_PRIORITY_HIGH` |
|
||||
|
||||
### Callback vs Main-Loop Apps
|
||||
|
||||
**Callback-only** (`hasMainLoop = false`):
|
||||
- `appMain()` runs in the shell's task 0, creates windows, registers event
|
||||
callbacks, and returns immediately.
|
||||
- All subsequent work happens through callbacks dispatched by `dvxUpdate()`.
|
||||
- Lifecycle ends when the last window is closed (detected by the
|
||||
`shellWrapDestroyWindow` wrapper).
|
||||
- No task stack allocated -- simpler and cheaper.
|
||||
- Examples: Program Manager, Notepad, Control Panel, DVX Demo, Image Viewer.
|
||||
|
||||
**Main-loop** (`hasMainLoop = true`):
|
||||
- A cooperative task is created via `tsCreate()`.
|
||||
- `appMain()` runs in that task with its own loop calling `tsYield()`.
|
||||
- Needed when the app has ongoing work that cannot be expressed purely as
|
||||
event callbacks (polling, computation, animation).
|
||||
- Lifecycle ends when `appMain()` returns.
|
||||
- Example: Clock (polls `time()` each second).
|
||||
|
||||
### Multi-Instance Support
|
||||
|
||||
DXE3's `dlopen` is reference-counted per path -- loading the same `.app`
|
||||
twice returns the same handle, sharing all globals and statics. For apps
|
||||
that set `multiInstance = true`, the shell copies the `.app` to a temp file
|
||||
(e.g., `C:\TEMP\_dvx02.app`) before `dlopen`, giving each instance its own
|
||||
code and data segment. Temp files are cleaned up on app termination.
|
||||
|
||||
### App State Machine
|
||||
|
||||
```
|
||||
Free -> Loaded -> Running -> Terminating -> Free
|
||||
```
|
||||
|
||||
- **Free**: slot available.
|
||||
- **Loaded**: DXE loaded, entry point not yet called (transient).
|
||||
- **Running**: entry point called, app is active.
|
||||
- **Terminating**: app's task returned or last window closed; awaiting reap.
|
||||
|
||||
## Resource Tracking
|
||||
|
||||
`sCurrentAppId` is a global set before calling any app code (entry, shutdown,
|
||||
callbacks). The shell's `dvxCreateWindow` wrapper stamps every new window with
|
||||
`win->appId = sCurrentAppId`. This is how the shell knows which windows belong
|
||||
to which app, enabling:
|
||||
|
||||
- Per-app window cleanup on crash or termination (walk the window stack,
|
||||
destroy all windows with matching `appId`).
|
||||
- The last-window-closes-app rule for callback-only apps: when
|
||||
`shellWrapDestroyWindow` detects that a callback-only app has no remaining
|
||||
windows, it marks the app `AppStateTerminatingE`.
|
||||
|
||||
`sCurrentAppId` is a simple global (not thread-local) because cooperative
|
||||
multitasking means only one task runs at a time.
|
||||
|
||||
## Task Manager
|
||||
|
||||
`shellTaskMgr.c` implements a shell-level Task Manager accessible via
|
||||
**Ctrl+Esc** regardless of which app is focused or whether the desktop app
|
||||
is running. It is owned by the shell (appId = 0), not by any DXE app.
|
||||
|
||||
Features:
|
||||
- **ListView** with 5 columns: Name, Title, File, Type (Task/Callback), Status.
|
||||
- **Switch To** -- find the app's topmost window, restore if minimized, raise
|
||||
and focus it.
|
||||
- **End Task** -- force-kill the selected app via `shellForceKillApp()`.
|
||||
- **Run...** -- open a file dialog filtered to `*.app`, load the selected file.
|
||||
- **Status bar** -- shows running app count and memory usage (total/free MB).
|
||||
- Registers with `shellRegisterDesktopUpdate` so the list auto-refreshes when
|
||||
apps load, terminate, or crash.
|
||||
|
||||
## Desktop Update Callbacks
|
||||
|
||||
`shellRegisterDesktopUpdate(fn)` adds a function pointer to a dynamic array
|
||||
(managed via stb_ds `arrput`/`arrdel`). `shellDesktopUpdate()` iterates the
|
||||
array and calls each registered function.
|
||||
|
||||
This is the mechanism by which the shell notifies interested parties (Program
|
||||
Manager status bar, Task Manager list) when app state changes -- without
|
||||
polling. Multiple listeners are supported. Listeners should call
|
||||
`shellUnregisterDesktopUpdate()` before they are destroyed.
|
||||
|
||||
## App Config Storage
|
||||
|
||||
Each app gets a dedicated writable config directory under `CONFIG/`. The path
|
||||
mirrors the app's location under `APPS/`:
|
||||
|
||||
```
|
||||
App path: APPS/PROGMAN/progman.app
|
||||
Config dir: CONFIG/PROGMAN/
|
||||
```
|
||||
|
||||
API:
|
||||
- `shellEnsureConfigDir(ctx)` -- create the directory tree via
|
||||
`platformMkdirRecursive()`. Returns 0 on success.
|
||||
- `shellConfigPath(ctx, "settings.ini", buf, sizeof(buf))` -- build a full
|
||||
path to a file in the config directory.
|
||||
|
||||
Apps use the standard preferences system (`prefsLoad`/`prefsSave`) pointed at
|
||||
their config directory for persistent settings.
|
||||
|
||||
## DXE Export Table
|
||||
|
||||
The shell registers a symbol export table via `dlregsym()` before loading any
|
||||
apps. Most symbols (all `dvx*`, `wgt*`, `ts*`, drawing functions, and required
|
||||
libc functions) are exported directly. `dvxCreateWindow` and `dvxDestroyWindow`
|
||||
are exported as wrappers that add resource tracking.
|
||||
`shellExport.c` contains the ABI contract between the shell and apps. Three
|
||||
categories of exports:
|
||||
|
||||
## Shell API
|
||||
1. **Wrapped functions** (3): `dvxCreateWindow`, `dvxCreateWindowCentered`,
|
||||
`dvxDestroyWindow`. These are intercepted to stamp `win->appId` for
|
||||
resource ownership tracking. Apps see them under their original names --
|
||||
the wrapping is transparent.
|
||||
|
||||
2. **Direct exports** (200+): all other `dvx*`, `wgt*`, `wm*`, `ts*`,
|
||||
drawing, preferences, platform, and shell API functions. Safe to call
|
||||
without interception.
|
||||
|
||||
3. **libc / libm / runtime exports** (200+): DXE3 modules are relocatable
|
||||
objects, not fully linked executables. Every C library function a DXE
|
||||
calls must be explicitly listed so the loader can resolve it at dlopen
|
||||
time. This includes:
|
||||
- Memory (malloc, calloc, realloc, free)
|
||||
- String operations (str*, mem*)
|
||||
- Formatted I/O (printf, snprintf, fprintf, sscanf, etc.)
|
||||
- File I/O (fopen, fread, fwrite, fclose, etc.)
|
||||
- Directory operations (opendir, readdir, closedir, mkdir)
|
||||
- Time (time, localtime, clock, strftime)
|
||||
- Math (sin, cos, sqrt, pow, floor, ceil, etc.)
|
||||
- stb_ds internals (arrput/arrfree/hm* macro backends)
|
||||
- stb_image / stb_image_write
|
||||
- libgcc 64-bit integer helpers (__divdi3, __moddi3, etc.)
|
||||
- DJGPP stdio internals (__dj_stdin, __dj_stdout, __dj_stderr)
|
||||
|
||||
The table is registered once via `dlregsym()` before any `dlopen()`.
|
||||
|
||||
## System Hotkeys
|
||||
|
||||
These are always active regardless of which app is focused:
|
||||
|
||||
| Hotkey | Action |
|
||||
|--------|--------|
|
||||
| Alt+Tab | Cycle windows forward (rotate top to bottom of stack) |
|
||||
| Shift+Alt+Tab | Cycle windows backward (pull bottom to top) |
|
||||
| Alt+F4 | Close the focused window (calls its onClose callback) |
|
||||
| Ctrl+F12 | Full screen screenshot -- prompts for save path (PNG/BMP/JPG) |
|
||||
| Ctrl+Shift+F12 | Focused window screenshot -- prompts for save path |
|
||||
| Ctrl+Esc | Open/raise the Task Manager (shell-level, always available) |
|
||||
| F10 | Activate/toggle the focused window's menu bar |
|
||||
| Alt+Space | Open/close the system menu on the focused window |
|
||||
|
||||
## Screenshot System
|
||||
|
||||
Two screenshot functions are available, both accessible from the system menu
|
||||
(Alt+Space) or via hotkeys:
|
||||
|
||||
- `interactiveScreenshot(ctx)` -- captures the full screen (composited
|
||||
backbuffer), then opens a Save As file dialog filtered to PNG/BMP/JPG/TGA.
|
||||
Called by Ctrl+F12 or the "Screenshot..." system menu item.
|
||||
- `interactiveWindowScreenshot(ctx, win)` -- captures just the focused
|
||||
window's content buffer. Called by Ctrl+Shift+F12 or the "Window Shot..."
|
||||
system menu item.
|
||||
|
||||
The system menu also includes standard window operations: Restore, Move, Size,
|
||||
Minimize, Maximize, and Close.
|
||||
|
||||
## Logging
|
||||
|
||||
`shellLog(fmt, ...)` appends a printf-formatted line to `dvx.log`. The file
|
||||
is opened, written, and closed on each call (append-per-write) so:
|
||||
- The file is never held open, allowing Notepad to read it while the shell
|
||||
runs.
|
||||
- Writes are flushed immediately, important for crash diagnostics.
|
||||
- The file is truncated once at startup.
|
||||
|
||||
Log output includes: startup sequence, preferences applied, video modes
|
||||
enumerated, system information, app load/reap events, crash details with
|
||||
full register dumps.
|
||||
|
||||
## Shell API Summary
|
||||
|
||||
| Function | Description |
|
||||
|----------|-------------|
|
||||
| `shellAppInit()` | Initialize the app slot table |
|
||||
| `shellLoadApp(ctx, path)` | Load and start an app from a `.app` file |
|
||||
| `shellReapApps(ctx)` | Clean up terminated apps (call each frame) |
|
||||
| `shellReapApp(ctx, app)` | Gracefully shut down a single app |
|
||||
| `shellForceKillApp(ctx, app)` | Forcibly kill an app (skip shutdown hook) |
|
||||
| `shellAppInit()` | Zero the 32-slot app table |
|
||||
| `shellLoadApp(ctx, path)` | Load and start a DXE app. Returns app ID (>= 1) or -1 |
|
||||
| `shellReapApps(ctx)` | Clean up terminated apps (call each frame). Returns true if any reaped |
|
||||
| `shellReapApp(ctx, app)` | Gracefully shut down one app (calls shutdown hook) |
|
||||
| `shellForceKillApp(ctx, app)` | Forcibly kill an app -- skips shutdown hook |
|
||||
| `shellTerminateAllApps(ctx)` | Kill all running apps (shell shutdown) |
|
||||
| `shellGetApp(appId)` | Get app slot by ID |
|
||||
| `shellRunningAppCount()` | Count running apps |
|
||||
| `shellLog(fmt, ...)` | Write to `dvx.log` |
|
||||
| `shellGetApp(appId)` | Get app slot by ID (NULL if invalid/free) |
|
||||
| `shellRunningAppCount()` | Count running apps (excluding the shell) |
|
||||
| `shellLog(fmt, ...)` | Append to dvx.log |
|
||||
| `shellEnsureConfigDir(ctx)` | Create an app's config directory tree |
|
||||
| `shellConfigPath(ctx, name, buf, size)` | Build path to file in app's config dir |
|
||||
| `shellExportInit()` | Register DXE symbol export table via dlregsym() |
|
||||
| `shellRegisterDesktopUpdate(fn)` | Register callback for app state changes |
|
||||
| `shellExportInit()` | Register DXE symbol export table |
|
||||
| `shellUnregisterDesktopUpdate(fn)` | Remove a previously registered callback |
|
||||
| `shellDesktopUpdate()` | Notify all registered listeners |
|
||||
| `shellTaskMgrOpen(ctx)` | Open or raise the Task Manager window |
|
||||
| `shellTaskMgrRefresh()` | Refresh the Task Manager list and status |
|
||||
| `shellInfoInit(ctx)` | Gather and log system hardware information |
|
||||
| `shellGetSystemInfo()` | Return cached system info text |
|
||||
|
|
|
|||
343
packet/README.md
343
packet/README.md
|
|
@ -1,77 +1,191 @@
|
|||
# Packet -- Reliable Serial Transport
|
||||
# Packet -- Reliable Serial Transport with HDLC Framing
|
||||
|
||||
Packetized serial transport providing reliable, ordered delivery over an
|
||||
unreliable serial link. Uses HDLC-style byte-stuffed framing, CRC-16-CCITT
|
||||
error detection, and a Go-Back-N sliding window protocol for automatic
|
||||
retransmission.
|
||||
|
||||
This layer sits on top of an already-open rs232 COM port. It does not
|
||||
open or close the serial port itself.
|
||||
|
||||
Packetized serial transport with HDLC-style framing, CRC-16 error
|
||||
detection, and a Go-Back-N sliding window protocol for reliable,
|
||||
ordered delivery over an unreliable serial link.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Application
|
||||
|
|
||||
[packet] framing, CRC, retransmit, ordering
|
||||
| pktSend() queue a packet for reliable delivery
|
||||
| pktPoll() receive, process ACKs/NAKs, check retransmit timers
|
||||
|
|
||||
[rs232] raw byte I/O
|
||||
[Packet Layer] framing, CRC, sequencing, sliding window ARQ
|
||||
|
|
||||
[rs232] raw byte I/O via ISR-driven ring buffers
|
||||
|
|
||||
UART
|
||||
```
|
||||
|
||||
The packet layer sits on top of an already-open rs232 COM port. It
|
||||
does not open or close the serial port itself.
|
||||
The packet layer adds framing, error detection, and reliability to the
|
||||
raw byte stream provided by rs232. The caller provides a receive callback
|
||||
that is invoked synchronously from `pktPoll()` for each complete, CRC-verified,
|
||||
in-order data packet.
|
||||
|
||||
|
||||
## Frame Format
|
||||
|
||||
Before byte stuffing:
|
||||
Before byte stuffing, each frame has the following layout:
|
||||
|
||||
```
|
||||
[0x7E] [SEQ] [TYPE] [LEN] [PAYLOAD...] [CRC_LO] [CRC_HI]
|
||||
```
|
||||
|
||||
| Field | Size | Description |
|
||||
|---------|---------|--------------------------------------|
|
||||
| `0x7E` | 1 byte | Frame delimiter (flag byte) |
|
||||
| `SEQ` | 1 byte | Sequence number (wrapping uint8) |
|
||||
| `TYPE` | 1 byte | Frame type (see below) |
|
||||
|-----------|-----------|----------------------------------------------|
|
||||
| `0x7E` | 1 byte | Flag byte -- frame delimiter |
|
||||
| `SEQ` | 1 byte | Sequence number (wrapping uint8_t, 0-255) |
|
||||
| `TYPE` | 1 byte | Frame type (DATA, ACK, NAK, RST) |
|
||||
| `LEN` | 1 byte | Payload length (0-255) |
|
||||
| Payload | 0-255 | Application data |
|
||||
| `CRC` | 2 bytes | CRC-16-CCITT over SEQ+TYPE+LEN+payload |
|
||||
| `CRC` | 2 bytes | CRC-16-CCITT, little-endian, over SEQ..payload |
|
||||
|
||||
The header is 3 bytes (SEQ + TYPE + LEN), the CRC is 2 bytes, so the
|
||||
minimum frame size (no payload) is 5 bytes. The maximum frame size (255-byte
|
||||
payload) is 260 bytes before byte stuffing.
|
||||
|
||||
### Frame Types
|
||||
|
||||
| Type | Value | Description |
|
||||
|--------|-------|----------------------------------------------|
|
||||
| `DATA` | 0x00 | Data frame carrying application payload |
|
||||
| `ACK` | 0x01 | Cumulative acknowledgment (next expected seq) |
|
||||
| `NAK` | 0x02 | Negative ack (request retransmit from seq) |
|
||||
| `RST` | 0x03 | Connection reset |
|
||||
|--------|-------|----------------------------------------------------|
|
||||
| `DATA` | 0x00 | Carries application payload |
|
||||
| `ACK` | 0x01 | Cumulative acknowledgment -- next expected sequence |
|
||||
| `NAK` | 0x02 | Negative ack -- request retransmit from this seq |
|
||||
| `RST` | 0x03 | Connection reset -- clear all state |
|
||||
|
||||
### Byte Stuffing
|
||||
|
||||
The flag byte (`0x7E`) and escape byte (`0x7D`) are escaped within
|
||||
frame data:
|
||||
HDLC transparency encoding ensures the flag byte (0x7E) and escape byte
|
||||
(0x7D) never appear in the frame body. Within the frame data (everything
|
||||
between flags), these two bytes are escaped by prefixing with 0x7D and
|
||||
XORing the original byte with 0x20:
|
||||
|
||||
- `0x7E` becomes `0x7D 0x5E`
|
||||
- `0x7D` becomes `0x7D 0x5D`
|
||||
|
||||
## Reliability
|
||||
In the worst case, every byte in the frame is escaped, doubling the wire
|
||||
size. In practice, these byte values are uncommon in typical data and the
|
||||
overhead is minimal.
|
||||
|
||||
The protocol uses Go-Back-N with a configurable sliding window
|
||||
(1-8 slots, default 4):
|
||||
### Why HDLC Framing
|
||||
|
||||
HDLC's flag-byte + byte-stuffing scheme is the simplest way to delimit
|
||||
variable-length frames on a raw byte stream. The 0x7E flag byte
|
||||
unambiguously marks frame boundaries. This is proven, lightweight, and
|
||||
requires zero buffering at the framing layer.
|
||||
|
||||
The alternative -- length-prefixed framing -- is fragile on noisy links
|
||||
because a corrupted length field permanently desynchronizes the receiver.
|
||||
With HDLC framing, the receiver can always resynchronize by hunting for
|
||||
the next flag byte.
|
||||
|
||||
|
||||
## CRC-16-CCITT
|
||||
|
||||
Error detection uses CRC-16-CCITT (polynomial 0x1021, initial value
|
||||
0xFFFF). The CRC covers the SEQ, TYPE, LEN, and payload fields. It is
|
||||
stored little-endian in the frame (CRC low byte first, then CRC high byte).
|
||||
|
||||
The CRC is computed via a 256-entry lookup table (512 bytes of `.rodata`).
|
||||
Table-driven CRC is approximately 10x faster than bit-by-bit computation
|
||||
on a 486 -- a worthwhile trade for a function called on every frame
|
||||
transmitted and received.
|
||||
|
||||
|
||||
## Go-Back-N Sliding Window Protocol
|
||||
|
||||
### Why Go-Back-N
|
||||
|
||||
Go-Back-N ARQ is simpler than Selective Repeat -- the receiver does not
|
||||
need an out-of-order reassembly buffer and only tracks a single expected
|
||||
sequence number. This works well for the low bandwidth-delay product of a
|
||||
serial link. On a 115200 bps local connection, the round-trip time is
|
||||
negligible, so the window rarely fills.
|
||||
|
||||
Go-Back-N's retransmit-all-from-NAK behavior wastes bandwidth on lossy
|
||||
links, but serial links are nearly lossless. The CRC check is primarily a
|
||||
safety net for electrical noise, not a routine error recovery mechanism.
|
||||
|
||||
### Protocol Details
|
||||
|
||||
The sliding window is configurable from 1 to 8 slots (default 4).
|
||||
Sequence numbers are 8-bit unsigned integers that wrap naturally at 256.
|
||||
The sequence space (256) is much larger than 2x the maximum window (16),
|
||||
so there is no ambiguity between old and new frames.
|
||||
|
||||
**Sender behavior:**
|
||||
- Assigns a monotonically increasing sequence number to each DATA frame
|
||||
- Retains a copy of each sent frame in a retransmit slot until it is
|
||||
acknowledged
|
||||
- When the window is full (`txCount >= windowSize`), blocks or returns
|
||||
`PKT_ERR_TX_FULL` depending on the `block` parameter
|
||||
|
||||
**Receiver behavior:**
|
||||
- Accepts frames strictly in order (`seq == rxExpectSeq`)
|
||||
- On in-order delivery, increments `rxExpectSeq` and sends an ACK
|
||||
carrying the new expected sequence number (cumulative acknowledgment)
|
||||
- Out-of-order frames within the window trigger a NAK for the expected
|
||||
sequence number
|
||||
- Duplicate and out-of-window frames are silently discarded
|
||||
|
||||
**ACK processing:**
|
||||
- ACKs carry the next expected sequence number (cumulative)
|
||||
- On receiving an ACK, the sender frees all retransmit slots with
|
||||
sequence numbers less than the ACK's sequence number
|
||||
|
||||
**NAK processing:**
|
||||
- A NAK requests retransmission from a specific sequence number
|
||||
- The sender retransmits that frame AND all subsequent unacknowledged
|
||||
frames (the Go-Back-N property)
|
||||
- Each retransmitted slot has its timer reset
|
||||
|
||||
**RST processing:**
|
||||
- Resets all sequence numbers and buffers to zero on both sides
|
||||
- The remote side also sends a RST in response
|
||||
|
||||
### Timer-Based Retransmission
|
||||
|
||||
Each retransmit slot tracks the time it was last (re)transmitted. If
|
||||
500ms elapses without an ACK, the slot is retransmitted and the timer
|
||||
is reset. This handles the case where an ACK or NAK was lost on the
|
||||
wire -- without this safety net, the connection would stall permanently.
|
||||
|
||||
The 500ms timeout is conservative for a local serial link (RTT is under
|
||||
1ms) but accounts for the remote side being busy processing. On BBS
|
||||
connections through the Linux proxy, the round-trip includes TCP latency,
|
||||
making the generous timeout appropriate.
|
||||
|
||||
### Receive State Machine
|
||||
|
||||
Incoming bytes from the serial port are fed through a three-state HDLC
|
||||
deframing state machine:
|
||||
|
||||
| State | Description |
|
||||
|----------|------------------------------------------------------|
|
||||
| `HUNT` | Discarding bytes until a flag (0x7E) is seen |
|
||||
| `ACTIVE` | Accumulating frame bytes; flag ends frame, ESC escapes |
|
||||
| `ESCAPE` | Previous byte was 0x7D; XOR this byte with 0x20 |
|
||||
|
||||
The flag byte serves double duty: it ends the current frame AND starts
|
||||
the next one. Back-to-back frames share a single flag byte, saving
|
||||
bandwidth. A frame is only processed if it meets the minimum size
|
||||
requirement (5 bytes), so spurious flags produce harmless zero-length
|
||||
"frames" that are discarded.
|
||||
|
||||
- **Sender** assigns sequential numbers to each DATA frame and retains
|
||||
a copy in the retransmit buffer until acknowledged.
|
||||
- **Receiver** delivers frames in order. Out-of-order frames trigger a
|
||||
NAK for the expected sequence number.
|
||||
- **ACK** carries the next expected sequence number (cumulative).
|
||||
- **NAK** triggers retransmission of the requested frame and all
|
||||
subsequent unacknowledged frames.
|
||||
- **Timer-based retransmit** fires after 500 poll cycles if no ACK or
|
||||
NAK has been received.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Types
|
||||
|
||||
```c
|
||||
// Receive callback -- called for each verified, in-order packet
|
||||
// Receive callback -- called for each verified, in-order data packet
|
||||
typedef void (*PktRecvCallbackT)(void *ctx, const uint8_t *data, int len);
|
||||
|
||||
// Opaque connection handle
|
||||
|
|
@ -81,8 +195,8 @@ typedef struct PktConnS PktConnT;
|
|||
### Constants
|
||||
|
||||
| Name | Value | Description |
|
||||
|-----------------------|-------|-------------------------------------|
|
||||
| `PKT_MAX_PAYLOAD` | 255 | Max payload bytes per packet |
|
||||
|------------------------|-------|--------------------------------------|
|
||||
| `PKT_MAX_PAYLOAD` | 255 | Maximum payload bytes per packet |
|
||||
| `PKT_DEFAULT_WINDOW` | 4 | Default sliding window size |
|
||||
| `PKT_MAX_WINDOW` | 8 | Maximum sliding window size |
|
||||
| `PKT_SUCCESS` | 0 | Success |
|
||||
|
|
@ -92,8 +206,9 @@ typedef struct PktConnS PktConnT;
|
|||
| `PKT_ERR_WOULD_BLOCK` | -4 | Operation would block |
|
||||
| `PKT_ERR_OVERFLOW` | -5 | Buffer overflow |
|
||||
| `PKT_ERR_INVALID_PARAM`| -6 | Invalid parameter |
|
||||
| `PKT_ERR_TX_FULL` | -7 | Transmit window full |
|
||||
| `PKT_ERR_TX_FULL` | -7 | Transmit window full (non-blocking) |
|
||||
| `PKT_ERR_NO_DATA` | -8 | No data available |
|
||||
| `PKT_ERR_DISCONNECTED` | -9 | Serial port disconnected or error |
|
||||
|
||||
### Functions
|
||||
|
||||
|
|
@ -106,12 +221,14 @@ PktConnT *pktOpen(int com, int windowSize,
|
|||
|
||||
Creates a packetized connection over an already-open COM port.
|
||||
|
||||
- `com` -- RS232 port index (`RS232_COM1`..`RS232_COM4`)
|
||||
- `windowSize` -- sliding window size (1-8), 0 for default (4)
|
||||
- `callback` -- called from `pktPoll()` for each received packet
|
||||
- `callbackCtx` -- user pointer passed to callback
|
||||
- `com` -- RS232 port index (`RS232_COM1` through `RS232_COM4`)
|
||||
- `windowSize` -- sliding window size (1-8), or 0 for the default (4)
|
||||
- `callback` -- called from `pktPoll()` for each received, verified,
|
||||
in-order data packet. The `data` pointer is valid only during the
|
||||
callback.
|
||||
- `callbackCtx` -- user pointer passed through to the callback
|
||||
|
||||
Returns a connection handle, or `NULL` on failure.
|
||||
Returns a connection handle, or `NULL` on failure (allocation error).
|
||||
|
||||
#### pktClose
|
||||
|
||||
|
|
@ -120,6 +237,7 @@ void pktClose(PktConnT *conn);
|
|||
```
|
||||
|
||||
Frees the connection state. Does **not** close the underlying COM port.
|
||||
The caller is responsible for calling `rs232Close()` separately.
|
||||
|
||||
#### pktSend
|
||||
|
||||
|
|
@ -127,12 +245,17 @@ Frees the connection state. Does **not** close the underlying COM port.
|
|||
int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block);
|
||||
```
|
||||
|
||||
Sends a packet. `len` must be 1..`PKT_MAX_PAYLOAD`.
|
||||
Sends a data packet. `len` must be in the range 1 to `PKT_MAX_PAYLOAD`
|
||||
(255). The data is copied into a retransmit slot before transmission, so
|
||||
the caller can reuse its buffer immediately.
|
||||
|
||||
- `block = true` -- waits for window space, polling for ACKs internally
|
||||
- `block = false` -- returns `PKT_ERR_TX_FULL` if the window is full
|
||||
- `block = true` -- If the transmit window is full, polls internally
|
||||
(calling `pktPoll()` in a tight loop) until an ACK frees a slot. Returns
|
||||
`PKT_ERR_DISCONNECTED` if the serial port drops during the wait.
|
||||
- `block = false` -- Returns `PKT_ERR_TX_FULL` immediately if the window
|
||||
is full.
|
||||
|
||||
The packet is stored in the retransmit buffer until acknowledged.
|
||||
Returns `PKT_SUCCESS` on success.
|
||||
|
||||
#### pktPoll
|
||||
|
||||
|
|
@ -140,11 +263,21 @@ The packet is stored in the retransmit buffer until acknowledged.
|
|||
int pktPoll(PktConnT *conn);
|
||||
```
|
||||
|
||||
Reads available serial data, processes received frames, sends ACKs and
|
||||
NAKs, and checks retransmit timers. Returns the number of DATA packets
|
||||
delivered to the callback.
|
||||
The main work function. Must be called frequently (every iteration of
|
||||
your main loop or event loop). It performs three tasks:
|
||||
|
||||
Must be called frequently (e.g. in your main loop).
|
||||
1. **Drain serial RX** -- reads all available bytes from the rs232 port
|
||||
and feeds them through the HDLC deframing state machine
|
||||
2. **Process frames** -- verifies CRC, handles DATA/ACK/NAK/RST frames,
|
||||
delivers data packets to the callback
|
||||
3. **Check retransmit timers** -- resends any slots that have timed out
|
||||
|
||||
Returns the number of DATA packets delivered to the callback this call,
|
||||
or `PKT_ERR_DISCONNECTED` if the serial port returned an error, or
|
||||
`PKT_ERR_INVALID_PARAM` if `conn` is NULL.
|
||||
|
||||
The callback is invoked synchronously, so the caller should be prepared
|
||||
for re-entrant calls to `pktSend()` from within the callback.
|
||||
|
||||
#### pktReset
|
||||
|
||||
|
|
@ -152,8 +285,19 @@ Must be called frequently (e.g. in your main loop).
|
|||
int pktReset(PktConnT *conn);
|
||||
```
|
||||
|
||||
Resets all sequence numbers and buffers to zero. Sends a RST frame to
|
||||
the remote side so it resets as well.
|
||||
Resets all sequence numbers, TX slots, and RX state to zero. Sends a RST
|
||||
frame to the remote side so it resets as well. Useful for recovering from
|
||||
a desynchronized state.
|
||||
|
||||
#### pktCanSend
|
||||
|
||||
```c
|
||||
bool pktCanSend(PktConnT *conn);
|
||||
```
|
||||
|
||||
Returns `true` if there is room in the transmit window for another
|
||||
packet. Useful for non-blocking send loops to avoid calling `pktSend()`
|
||||
when it would return `PKT_ERR_TX_FULL`.
|
||||
|
||||
#### pktGetPending
|
||||
|
||||
|
|
@ -162,32 +306,37 @@ int pktGetPending(PktConnT *conn);
|
|||
```
|
||||
|
||||
Returns the number of unacknowledged packets currently in the transmit
|
||||
window. Useful for throttling sends in non-blocking mode.
|
||||
window. Ranges from 0 (all sent packets acknowledged) to `windowSize`
|
||||
(window full). Useful for throttling sends and monitoring link health.
|
||||
|
||||
## Example
|
||||
|
||||
## Usage Example
|
||||
|
||||
```c
|
||||
#include "packet.h"
|
||||
#include "../rs232/rs232.h"
|
||||
|
||||
void onPacket(void *ctx, const uint8_t *data, int len) {
|
||||
// process received packet
|
||||
// process received packet -- data is valid only during this callback
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
// Open serial port first
|
||||
// Open serial port first (packet layer does not manage it)
|
||||
rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE);
|
||||
|
||||
// Create packet connection with default window size
|
||||
// Create packet connection with default window size (4)
|
||||
PktConnT *conn = pktOpen(RS232_COM1, 0, onPacket, NULL);
|
||||
|
||||
// Send a packet (blocking)
|
||||
// Send a packet (blocking -- waits for window space if needed)
|
||||
uint8_t msg[] = "Hello, packets!";
|
||||
pktSend(conn, msg, sizeof(msg), true);
|
||||
|
||||
// Main loop
|
||||
// Main loop -- must call pktPoll() frequently
|
||||
while (1) {
|
||||
int delivered = pktPoll(conn);
|
||||
if (delivered == PKT_ERR_DISCONNECTED) {
|
||||
break;
|
||||
}
|
||||
// delivered = number of packets received this iteration
|
||||
}
|
||||
|
||||
|
|
@ -197,11 +346,55 @@ int main(void) {
|
|||
}
|
||||
```
|
||||
|
||||
## CRC
|
||||
### Non-Blocking Send Pattern
|
||||
|
||||
```c
|
||||
// Send as fast as the window allows, doing other work between sends
|
||||
while (bytesLeft > 0) {
|
||||
pktPoll(conn); // process ACKs, free window slots
|
||||
|
||||
if (pktCanSend(conn)) {
|
||||
int chunk = bytesLeft;
|
||||
if (chunk > PKT_MAX_PAYLOAD) {
|
||||
chunk = PKT_MAX_PAYLOAD;
|
||||
}
|
||||
|
||||
if (pktSend(conn, data + offset, chunk, false) == PKT_SUCCESS) {
|
||||
offset += chunk;
|
||||
bytesLeft -= chunk;
|
||||
}
|
||||
}
|
||||
|
||||
// do other work here (update UI, check for cancel, etc.)
|
||||
}
|
||||
|
||||
// Drain remaining ACKs
|
||||
while (pktGetPending(conn) > 0) {
|
||||
pktPoll(conn);
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Internal Data Structures
|
||||
|
||||
### Connection State (PktConnT)
|
||||
|
||||
The connection handle contains:
|
||||
|
||||
- **COM port index** and **window size** (configuration)
|
||||
- **Callback** function pointer and context
|
||||
- **TX state**: next sequence to assign, oldest unacked sequence, array
|
||||
of retransmit slots, count of slots in use
|
||||
- **RX state**: next expected sequence, deframing state machine state,
|
||||
frame accumulation buffer
|
||||
|
||||
### Retransmit Slots (TxSlotT)
|
||||
|
||||
Each slot holds a copy of the sent payload, its sequence number, payload
|
||||
length, and a `clock_t` timestamp of when it was last transmitted. The
|
||||
retransmit timer compares this timestamp against the current time to
|
||||
detect timeout.
|
||||
|
||||
CRC-16-CCITT (polynomial 0x1021, init 0xFFFF) computed via a 256-entry
|
||||
lookup table (512 bytes). The CRC covers the SEQ, TYPE, LEN, and
|
||||
payload fields.
|
||||
|
||||
## Building
|
||||
|
||||
|
|
@ -210,6 +403,26 @@ make # builds ../lib/libpacket.a
|
|||
make clean # removes objects and library
|
||||
```
|
||||
|
||||
Requires `librs232.a` at link time.
|
||||
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
|
||||
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
|
||||
|
||||
Target: DJGPP cross-compiler, 486+ CPU.
|
||||
Objects are placed in `../obj/packet/`, the library in `../lib/`.
|
||||
|
||||
Requires `librs232.a` at link time (for `rs232Read()` and `rs232Write()`).
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
- `packet.h` -- Public API header (types, constants, function prototypes)
|
||||
- `packet.c` -- Complete implementation (framing, CRC, ARQ, state machine)
|
||||
- `Makefile` -- DJGPP cross-compilation build rules
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `rs232/` -- Serial port I/O (must be linked: `-lrs232`)
|
||||
|
||||
## Used By
|
||||
|
||||
- `seclink/` -- Secure serial link (adds channel multiplexing and encryption)
|
||||
- `proxy/` -- Linux serial proxy (uses a socket-based adaptation)
|
||||
|
|
|
|||
141
proxy/README.md
141
proxy/README.md
|
|
@ -1,16 +1,20 @@
|
|||
# SecLink Proxy
|
||||
# SecLink Proxy -- Linux Serial Bridge
|
||||
|
||||
Linux-hosted proxy that bridges an 86Box emulated serial port to a
|
||||
remote telnet BBS. The 86Box side communicates using the secLink
|
||||
protocol (packet framing, DH key exchange, XTEA encryption). The BBS
|
||||
side is plain telnet over TCP.
|
||||
remote telnet BBS. Part of the DVX GUI project.
|
||||
|
||||
The 86Box side communicates using the SecLink protocol (HDLC packet
|
||||
framing, CRC-16, DH key exchange, XTEA-CTR encryption). The BBS side
|
||||
is plain telnet over TCP. All crypto is transparent to the BBS -- it
|
||||
sees a normal telnet client.
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
86Box (DOS terminal) Remote BBS
|
||||
| |
|
||||
emulated modem telnet:23
|
||||
emulated modem telnet
|
||||
| |
|
||||
TCP:2323 TCP:23
|
||||
| |
|
||||
|
|
@ -20,10 +24,11 @@ side is plain telnet over TCP.
|
|||
```
|
||||
|
||||
The proxy accepts a single TCP connection from 86Box, performs the
|
||||
secLink handshake (DH key exchange), then connects to the BBS. All
|
||||
traffic between 86Box and the proxy is encrypted via XTEA-CTR on
|
||||
channel 0. Traffic between the proxy and the BBS is unencrypted
|
||||
telnet.
|
||||
SecLink handshake (Diffie-Hellman key exchange), then connects to the
|
||||
BBS. All traffic between 86Box and the proxy is encrypted via XTEA-CTR
|
||||
on channel 0. Traffic between the proxy and the BBS is unencrypted
|
||||
telnet with IAC negotiation handling.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
@ -32,10 +37,12 @@ secproxy [listen_port] [bbs_host] [bbs_port]
|
|||
```
|
||||
|
||||
| Argument | Default | Description |
|
||||
|---------------|------------------------|---------------------------------|
|
||||
|---------------|--------------|---------------------------------|
|
||||
| `listen_port` | 2323 | TCP port for 86Box connection |
|
||||
| `bbs_host` | bbs.duensing.digital | BBS hostname |
|
||||
| `bbs_port` | 23 | BBS TCP port |
|
||||
| `bbs_host` | 10.1.0.244 | BBS hostname or IP |
|
||||
| `bbs_port` | 2023 | BBS TCP port |
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
secproxy # all defaults
|
||||
|
|
@ -44,15 +51,20 @@ secproxy 2323 bbs.example.com 23 # different BBS
|
|||
secproxy --help # show usage
|
||||
```
|
||||
|
||||
|
||||
## Startup Sequence
|
||||
|
||||
1. Listen on the configured TCP port
|
||||
2. Wait for 86Box to connect (blocks on accept)
|
||||
3. Connect to the remote BBS
|
||||
4. Seed the RNG from `/dev/urandom`
|
||||
5. Open secLink and perform the DH handshake (blocks until the DOS
|
||||
side completes its handshake)
|
||||
6. Enter the proxy loop
|
||||
1. Listen on the configured TCP port.
|
||||
2. Wait for 86Box to connect (polls with Ctrl+C support).
|
||||
3. Map the TCP socket to COM0 via the socket shim.
|
||||
4. Seed the RNG from `/dev/urandom`.
|
||||
5. Open SecLink and perform the DH handshake (blocks until the DOS
|
||||
side completes its handshake).
|
||||
6. Wait for the terminal to send ENTER (so the user can confirm the
|
||||
connection is working before BBS output starts).
|
||||
7. Connect to the remote BBS.
|
||||
8. Enter the proxy loop.
|
||||
|
||||
|
||||
## Proxy Loop
|
||||
|
||||
|
|
@ -62,47 +74,84 @@ the two TCP connections:
|
|||
- **86Box -> BBS**: `secLinkPoll()` reads from the 86Box socket via
|
||||
the socket shim, decrypts incoming packets, and the receive callback
|
||||
writes plaintext to the BBS socket.
|
||||
- **BBS -> 86Box**: `read()` from the BBS socket, then
|
||||
`secLinkSend()` encrypts and sends to 86Box via the socket shim.
|
||||
- **Maintenance**: `secLinkPoll()` also handles packet-layer retransmit
|
||||
timers on every iteration.
|
||||
- **BBS -> 86Box**: `read()` from the BBS socket, then the telnet
|
||||
filter strips IAC sequences, then `secLinkSend()` encrypts and sends
|
||||
to 86Box via the socket shim. If the send window is full, the loop
|
||||
retries with ACK processing until the data goes through.
|
||||
- **Maintenance**: `secLinkPoll()` also handles packet-layer
|
||||
retransmit timers on every iteration.
|
||||
|
||||
The proxy exits cleanly on Ctrl+C (SIGINT), SIGTERM, or when either
|
||||
side disconnects.
|
||||
|
||||
## 86Box Configuration
|
||||
|
||||
Configure the 86Box serial port to use a telnet connection:
|
||||
## Telnet Negotiation
|
||||
|
||||
The proxy handles RFC 854 telnet IAC sequences from the BBS so they
|
||||
do not corrupt the terminal display. A state machine parser strips
|
||||
IAC sequences from the data stream, persisting state across TCP
|
||||
segment boundaries.
|
||||
|
||||
Accepted options:
|
||||
|
||||
- ECHO (option 1) -- server echoes characters
|
||||
- SGA (option 3) -- suppress go-ahead for character-at-a-time mode
|
||||
- TTYPE (option 24) -- terminal type negotiation
|
||||
- NAWS (option 31) -- window size negotiation
|
||||
|
||||
All other options are refused. Subnegotiations (SB...SE) are consumed
|
||||
silently.
|
||||
|
||||
1. In 86Box settings, set a COM port to "TCP (server)" or
|
||||
"TCP (client)" mode pointing at the proxy's listen port
|
||||
2. Enable "No telnet negotiation" to send raw bytes
|
||||
3. The DOS terminal application running inside 86Box uses secLink
|
||||
over this serial port
|
||||
|
||||
## Socket Shim
|
||||
|
||||
The proxy reuses the same packet, security, and secLink source code
|
||||
as the DOS build. A socket shim (`sockShim.h`/`sockShim.c`) provides
|
||||
rs232-compatible `rs232Read()`/`rs232Write()` functions backed by TCP
|
||||
sockets instead of UART hardware:
|
||||
The proxy reuses the same packet, security, and secLink source code as
|
||||
the DOS build. A socket shim (`sockShim.h` / `sockShim.c`) provides
|
||||
rs232-compatible functions backed by TCP sockets instead of UART
|
||||
hardware:
|
||||
|
||||
| rs232 function | Socket shim behavior |
|
||||
|----------------|-----------------------------------------------|
|
||||
| `rs232Open()` | No-op (socket already connected) |
|
||||
| `rs232Close()` | Marks port closed (socket managed by caller) |
|
||||
| `rs232Open()` | Validates socket assigned; ignores serial params |
|
||||
| `rs232Close()` | Marks port closed (socket lifecycle is caller's) |
|
||||
| `rs232Read()` | Non-blocking `recv()` with `MSG_DONTWAIT` |
|
||||
| `rs232Write()` | Blocking `send()` loop with `MSG_NOSIGNAL` |
|
||||
|
||||
The shim maps COM port indices (0-3) to socket file descriptors via
|
||||
`sockShimSetFd()`, which must be called before opening the secLink
|
||||
layer.
|
||||
`sockShimSetFd()`, which must be called before opening the SecLink
|
||||
layer. Up to 4 ports are supported.
|
||||
|
||||
The Makefile uses `-include sockShim.h` when compiling the packet and
|
||||
secLink layers, which defines `RS232_H` to prevent the real `rs232.h`
|
||||
from being included.
|
||||
|
||||
|
||||
## DJGPP Stubs
|
||||
|
||||
DOS-specific headers required by the security library are replaced by
|
||||
minimal stubs in `stubs/`:
|
||||
|
||||
| Stub | Replaces DJGPP header | Contents |
|
||||
|-------------------|------------------------|-------------------|
|
||||
| `stubs/pc.h` | `<pc.h>` | No-op definitions |
|
||||
| `stubs/go32.h` | `<go32.h>` | No-op definitions |
|
||||
| `stubs/sys/farptr.h` | `<sys/farptr.h>` | No-op definitions |
|
||||
|
||||
The security library's hardware entropy function returns zeros on
|
||||
Linux, which is harmless since the proxy seeds the RNG from
|
||||
`/dev/urandom` before the handshake.
|
||||
|
||||
|
||||
## 86Box Configuration
|
||||
|
||||
Configure the 86Box serial port to connect to the proxy:
|
||||
|
||||
1. In 86Box settings, set a COM port to TCP client mode pointing at
|
||||
the proxy's listen port (default 2323).
|
||||
2. Enable "No telnet negotiation" to send raw bytes.
|
||||
3. The DOS terminal application running inside 86Box uses SecLink
|
||||
over this serial port.
|
||||
|
||||
DOS-specific headers (`<pc.h>`, `<go32.h>`, `<sys/farptr.h>`) are
|
||||
replaced by minimal stubs in `stubs/` that provide no-op
|
||||
implementations. The security library's hardware entropy function
|
||||
returns zeros on Linux, which is harmless since the proxy seeds the
|
||||
RNG from `/dev/urandom` before the handshake.
|
||||
|
||||
## Building
|
||||
|
||||
|
|
@ -114,7 +163,9 @@ make clean # removes objects and binary
|
|||
Objects are placed in `../obj/proxy/`, the binary in `../bin/`.
|
||||
|
||||
Requires only a standard Linux C toolchain (gcc, libc). No external
|
||||
dependencies.
|
||||
dependencies beyond the project's own packet, security, and secLink
|
||||
source files, which are compiled directly from their source directories.
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
|
|
@ -123,7 +174,7 @@ proxy/
|
|||
proxy.c main proxy program
|
||||
sockShim.h rs232-compatible socket API (header)
|
||||
sockShim.c socket shim implementation
|
||||
Makefile Linux build
|
||||
Makefile Linux native build
|
||||
stubs/
|
||||
pc.h stub for DJGPP <pc.h>
|
||||
go32.h stub for DJGPP <go32.h>
|
||||
|
|
|
|||
370
rs232/README.md
370
rs232/README.md
|
|
@ -1,63 +1,187 @@
|
|||
# RS232 -- Serial Port Library for DJGPP
|
||||
# RS232 -- ISR-Driven Serial Port Library for DJGPP
|
||||
|
||||
ISR-driven UART communication library supporting up to 4 simultaneous
|
||||
COM ports with ring buffers and hardware/software flow control.
|
||||
Interrupt-driven UART communication library supporting up to 4 simultaneous
|
||||
COM ports with ring buffers and hardware/software flow control. Targets
|
||||
486-class DOS hardware running under DJGPP/DPMI.
|
||||
|
||||
Ported from the DOS Serial Library 1.4 by Karl Stenerud (MIT License),
|
||||
stripped to DJGPP-only codepaths and restyled.
|
||||
stripped to DJGPP-only codepaths and restyled for the DVX project.
|
||||
|
||||
## Features
|
||||
|
||||
- ISR-driven receive and transmit with 2048-byte ring buffers
|
||||
- Auto-detected IRQ from BIOS data area
|
||||
- UART type detection (8250, 16450, 16550, 16550A)
|
||||
- 16550 FIFO detection and configurable trigger threshold
|
||||
- XON/XOFF, RTS/CTS, and DTR/DSR flow control
|
||||
- DPMI memory locking for ISR safety
|
||||
- Speeds from 50 to 115200 bps
|
||||
- 5-8 data bits, N/O/E/M/S parity, 1-2 stop bits
|
||||
## Architecture
|
||||
|
||||
The library is built around a single shared ISR (`comGeneralIsr`) that
|
||||
services all open COM ports. This design is necessary because COM1/COM3
|
||||
typically share IRQ4 and COM2/COM4 share IRQ3 -- a single handler that
|
||||
polls all ports avoids the complexity of per-IRQ dispatch.
|
||||
|
||||
```
|
||||
Application
|
||||
|
|
||||
| rs232Read() non-blocking drain from RX ring buffer
|
||||
| rs232Write() blocking polled write directly to UART THR
|
||||
| rs232WriteBuf() non-blocking write into TX ring buffer
|
||||
|
|
||||
[Ring Buffers] 2048-byte RX + TX per port, power-of-2 bitmask indexing
|
||||
|
|
||||
[ISR] comGeneralIsr -- shared handler for all open ports
|
||||
|
|
||||
[UART] 8250 / 16450 / 16550 / 16550A hardware
|
||||
```
|
||||
|
||||
### ISR Design
|
||||
|
||||
The ISR follows a careful protocol to remain safe under DPMI while
|
||||
keeping the system responsive:
|
||||
|
||||
1. **Mask** all COM port IRQs on the PIC to prevent ISR re-entry
|
||||
2. **STI** to allow higher-priority interrupts (timer tick, keyboard) through
|
||||
3. **Loop** over all open ports, draining each UART's pending interrupt
|
||||
conditions (data ready, TX hold empty, modem status, line status)
|
||||
4. **CLI**, send EOI to the PIC, re-enable COM IRQs, **STI** before IRET
|
||||
|
||||
This mask-then-STI pattern is standard for slow device ISRs on PC
|
||||
hardware. It prevents the same IRQ from re-entering while allowing the
|
||||
system timer and keyboard to function during UART processing.
|
||||
|
||||
### Ring Buffers
|
||||
|
||||
Both RX and TX buffers are 2048 bytes, sized as a power of 2 so that
|
||||
head/tail wraparound is a single AND operation (bitmask indexing) rather
|
||||
than an expensive modulo -- critical for ISR-speed code on a 486.
|
||||
|
||||
The buffers use a one-slot-wasted design to distinguish full from empty:
|
||||
`head == tail` means empty, `(head + 1) & MASK == tail` means full.
|
||||
|
||||
### Flow Control
|
||||
|
||||
Flow control operates entirely within the ISR using watermark thresholds.
|
||||
When the RX buffer crosses 80% full, the ISR signals the remote side to
|
||||
stop sending; when it drops below 20%, the ISR allows the remote to
|
||||
resume. This prevents buffer overflow without any application involvement.
|
||||
|
||||
Three modes are supported:
|
||||
|
||||
| Mode | Stop Signal | Resume Signal |
|
||||
|------------|-------------------|-------------------|
|
||||
| XON/XOFF | Send XOFF (0x13) | Send XON (0x11) |
|
||||
| RTS/CTS | Deassert RTS | Assert RTS |
|
||||
| DTR/DSR | Deassert DTR | Assert DTR |
|
||||
|
||||
On the TX side, the ISR monitors incoming XON/XOFF bytes and the CTS/DSR
|
||||
modem status lines to pause and resume transmission from the TX ring
|
||||
buffer.
|
||||
|
||||
### DPMI Memory Locking
|
||||
|
||||
The ISR code and all per-port state structures (`sComPorts` array) are
|
||||
locked in physical memory via `__dpmi_lock_linear_region`. This prevents
|
||||
page faults during interrupt handling -- a hard requirement for any ISR
|
||||
running under a DPMI host (DOS extender, Windows 3.x, OS/2 VDM, etc.).
|
||||
An IRET wrapper is allocated by DPMI to handle the real-mode to
|
||||
protected-mode transition on hardware interrupt entry.
|
||||
|
||||
|
||||
## UART Type Detection
|
||||
|
||||
`rs232GetUartType()` probes the UART hardware to identify the chip:
|
||||
|
||||
1. **Scratch register test** -- Writes two known values (0xAA, 0x55) to
|
||||
UART register 7 and reads them back. The 8250 lacks this register, so
|
||||
readback fails. If both values read back correctly, the UART is at
|
||||
least a 16450.
|
||||
|
||||
2. **FIFO test** -- Enables the FIFO via the FCR (FIFO Control Register),
|
||||
then reads bits 7:6 of the IIR (Interrupt Identification Register):
|
||||
- `0b11` = 16550A (working 16-byte FIFO)
|
||||
- `0b10` = 16550 (broken FIFO -- present in hardware but unusable)
|
||||
- `0b00` = 16450 (no FIFO at all)
|
||||
|
||||
The original FCR value is restored after probing.
|
||||
|
||||
| Constant | Value | Description |
|
||||
|---------------------|-------|----------------------------------------|
|
||||
| `RS232_UART_UNKNOWN`| 0 | Unknown or undetected |
|
||||
| `RS232_UART_8250` | 1 | Original IBM PC -- no FIFO, no scratch |
|
||||
| `RS232_UART_16450` | 2 | Scratch register present, no FIFO |
|
||||
| `RS232_UART_16550` | 3 | Broken FIFO (rare, unusable) |
|
||||
| `RS232_UART_16550A` | 4 | Working 16-byte FIFO (most common) |
|
||||
|
||||
On 16550A UARTs, the FIFO trigger threshold is configurable via
|
||||
`rs232SetFifoThreshold()` with levels of 1, 4, 8, or 14 bytes. The
|
||||
default is 14, which minimizes interrupt overhead at high baud rates.
|
||||
|
||||
|
||||
## IRQ Auto-Detection
|
||||
|
||||
When `rs232Open()` is called without a prior `rs232SetIrq()` override,
|
||||
the library auto-detects the UART's IRQ by:
|
||||
|
||||
1. Saving the current PIC interrupt mask registers (IMR)
|
||||
2. Enabling all IRQ lines on both PICs
|
||||
3. Generating a TX Hold Empty interrupt on the UART
|
||||
4. Reading the PIC's Interrupt Request Register (IRR) to see which line
|
||||
went high
|
||||
5. Disabling the interrupt, reading IRR again to mask out persistent bits
|
||||
6. Re-enabling once more to confirm the detection
|
||||
7. Restoring the original PIC mask
|
||||
|
||||
If auto-detection fails (common on virtualized hardware that does not
|
||||
model the IRR accurately), the library falls back to the default IRQ for
|
||||
the port (IRQ4 for COM1/COM3, IRQ3 for COM2/COM4).
|
||||
|
||||
The BIOS Data Area (at segment 0x0040) is read to determine each port's
|
||||
I/O base address. Ports not configured in the BDA are unavailable.
|
||||
|
||||
|
||||
## COM Port Support
|
||||
|
||||
| Constant | Value | Default IRQ | Default Base |
|
||||
|--------------|-------|-------------|--------------|
|
||||
| `RS232_COM1` | 0 | IRQ 4 | 0x3F8 |
|
||||
| `RS232_COM2` | 1 | IRQ 3 | 0x2F8 |
|
||||
| `RS232_COM3` | 2 | IRQ 4 | 0x3E8 |
|
||||
| `RS232_COM4` | 3 | IRQ 3 | 0x2E8 |
|
||||
|
||||
Base addresses are read from the BIOS Data Area at runtime. The default
|
||||
IRQ values are used only as a fallback when auto-detection fails. Both
|
||||
the base address and IRQ can be overridden before opening with
|
||||
`rs232SetBase()` and `rs232SetIrq()`.
|
||||
|
||||
|
||||
## Supported Baud Rates
|
||||
|
||||
All standard rates from 50 to 115200 bps are supported. The baud rate
|
||||
divisor is computed from the standard 1.8432 MHz UART crystal:
|
||||
|
||||
| Rate | Divisor | Rate | Divisor |
|
||||
|--------|---------|--------|---------|
|
||||
| 50 | 2304 | 4800 | 24 |
|
||||
| 75 | 1536 | 7200 | 16 |
|
||||
| 110 | 1047 | 9600 | 12 |
|
||||
| 150 | 768 | 19200 | 6 |
|
||||
| 300 | 384 | 38400 | 3 |
|
||||
| 600 | 192 | 57600 | 2 |
|
||||
| 1200 | 96 | 115200 | 1 |
|
||||
| 1800 | 64 | | |
|
||||
| 2400 | 48 | | |
|
||||
| 3800 | 32 | | |
|
||||
|
||||
Data bits (5-8), parity (N/O/E/M/S), and stop bits (1-2) are configured
|
||||
by writing the appropriate LCR (Line Control Register) bits.
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
### Types
|
||||
|
||||
All functions take a COM port index (`int com`) as their first argument:
|
||||
|
||||
| Constant | Value | Description |
|
||||
|--------------|-------|-------------|
|
||||
| `RS232_COM1` | 0 | COM1 |
|
||||
| `RS232_COM2` | 1 | COM2 |
|
||||
| `RS232_COM3` | 2 | COM3 |
|
||||
| `RS232_COM4` | 3 | COM4 |
|
||||
|
||||
### UART Types
|
||||
|
||||
| Constant | Value | Description |
|
||||
|---------------------|-------|--------------------------------------|
|
||||
| `RS232_UART_UNKNOWN`| 0 | Unknown or undetected |
|
||||
| `RS232_UART_8250` | 1 | 8250 -- no FIFO, no scratch register |
|
||||
| `RS232_UART_16450` | 2 | 16450 -- scratch register, no FIFO |
|
||||
| `RS232_UART_16550` | 3 | 16550 -- broken FIFO (unusable) |
|
||||
| `RS232_UART_16550A` | 4 | 16550A -- working 16-byte FIFO |
|
||||
|
||||
### Handshaking Modes
|
||||
|
||||
| Constant | Value | Description |
|
||||
|--------------------------|-------|---------------------------|
|
||||
| `RS232_HANDSHAKE_NONE` | 0 | No flow control |
|
||||
| `RS232_HANDSHAKE_XONXOFF`| 1 | Software (XON/XOFF) |
|
||||
| `RS232_HANDSHAKE_RTSCTS` | 2 | Hardware (RTS/CTS) |
|
||||
| `RS232_HANDSHAKE_DTRDSR` | 3 | Hardware (DTR/DSR) |
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Constant | Value | Description |
|
||||
|-------------------------------|-------|---------------------------|
|
||||
|-------------------------------|-------|--------------------------|
|
||||
| `RS232_SUCCESS` | 0 | Success |
|
||||
| `RS232_ERR_UNKNOWN` | -1 | Unknown error |
|
||||
| `RS232_ERR_NOT_OPEN` | -2 | Port not open |
|
||||
| `RS232_ERR_ALREADY_OPEN` | -3 | Port already open |
|
||||
| `RS232_ERR_NO_UART` | -4 | No UART detected |
|
||||
| `RS232_ERR_NO_UART` | -4 | No UART detected at base |
|
||||
| `RS232_ERR_INVALID_PORT` | -5 | Bad port index |
|
||||
| `RS232_ERR_INVALID_BASE` | -6 | Bad I/O base address |
|
||||
| `RS232_ERR_INVALID_IRQ` | -7 | Bad IRQ number |
|
||||
|
|
@ -71,20 +195,29 @@ All functions take a COM port index (`int com`) as their first argument:
|
|||
| `RS232_ERR_IRQ_NOT_FOUND` | -15 | Could not detect IRQ |
|
||||
| `RS232_ERR_LOCK_MEM` | -16 | DPMI memory lock failed |
|
||||
|
||||
### Functions
|
||||
### Handshaking Modes
|
||||
|
||||
#### Open / Close
|
||||
| Constant | Value | Description |
|
||||
|---------------------------|-------|----------------------|
|
||||
| `RS232_HANDSHAKE_NONE` | 0 | No flow control |
|
||||
| `RS232_HANDSHAKE_XONXOFF` | 1 | Software (XON/XOFF) |
|
||||
| `RS232_HANDSHAKE_RTSCTS` | 2 | Hardware (RTS/CTS) |
|
||||
| `RS232_HANDSHAKE_DTRDSR` | 3 | Hardware (DTR/DSR) |
|
||||
|
||||
### Open / Close
|
||||
|
||||
```c
|
||||
int rs232Open(int com, int32_t bps, int dataBits, char parity,
|
||||
int stopBits, int handshake);
|
||||
```
|
||||
|
||||
Opens a COM port. Detects the UART base address from the BIOS data
|
||||
area, auto-detects the IRQ, installs the ISR, and configures the port.
|
||||
Opens a COM port. Reads the UART base address from the BIOS data area,
|
||||
auto-detects the IRQ, locks ISR memory via DPMI, installs the ISR, and
|
||||
configures the UART for the specified parameters. Returns `RS232_SUCCESS`
|
||||
or an error code.
|
||||
|
||||
- `bps` -- baud rate (50, 75, 110, 150, 300, 600, 1200, 1800, 2400,
|
||||
3800, 4800, 7200, 9600, 19200, 38400, 57600, 115200)
|
||||
- `com` -- port index (`RS232_COM1` through `RS232_COM4`)
|
||||
- `bps` -- baud rate (50 through 115200)
|
||||
- `dataBits` -- 5, 6, 7, or 8
|
||||
- `parity` -- `'N'` (none), `'O'` (odd), `'E'` (even), `'M'` (mark),
|
||||
`'S'` (space)
|
||||
|
|
@ -95,42 +228,50 @@ area, auto-detects the IRQ, installs the ISR, and configures the port.
|
|||
int rs232Close(int com);
|
||||
```
|
||||
|
||||
Closes the port, removes the ISR, and restores the original interrupt
|
||||
vector.
|
||||
Closes the port, disables UART interrupts, removes the ISR, restores the
|
||||
original interrupt vector, and unlocks DPMI memory (when the last port
|
||||
closes).
|
||||
|
||||
#### Read / Write
|
||||
### Read / Write
|
||||
|
||||
```c
|
||||
int rs232Read(int com, char *data, int len);
|
||||
```
|
||||
|
||||
Reads up to `len` bytes from the receive buffer. Returns the number of
|
||||
bytes actually read (0 if the buffer is empty).
|
||||
Non-blocking read. Drains up to `len` bytes from the RX ring buffer.
|
||||
Returns the number of bytes actually read (0 if the buffer is empty).
|
||||
If flow control is active and the buffer drops below the low-water mark,
|
||||
the ISR will re-enable receive from the remote side.
|
||||
|
||||
```c
|
||||
int rs232Write(int com, const char *data, int len);
|
||||
```
|
||||
|
||||
Blocking write. Sends `len` bytes, waiting for transmit buffer space
|
||||
as needed. Returns `RS232_SUCCESS` or an error code.
|
||||
Blocking polled write. Sends `len` bytes directly to the UART THR
|
||||
(Transmit Holding Register), bypassing the TX ring buffer entirely.
|
||||
Polls LSR for THR empty before each byte. Returns `RS232_SUCCESS` or
|
||||
an error code.
|
||||
|
||||
```c
|
||||
int rs232WriteBuf(int com, const char *data, int len);
|
||||
```
|
||||
|
||||
Non-blocking write. Copies as many bytes as will fit into the transmit
|
||||
buffer. Returns the number of bytes actually queued.
|
||||
Non-blocking buffered write. Copies as many bytes as will fit into the
|
||||
TX ring buffer. The ISR drains the TX buffer to the UART automatically.
|
||||
Returns the number of bytes actually queued. If the buffer is full, some
|
||||
bytes may be dropped.
|
||||
|
||||
#### Buffer Management
|
||||
### Buffer Management
|
||||
|
||||
```c
|
||||
int rs232ClearRxBuffer(int com);
|
||||
int rs232ClearTxBuffer(int com);
|
||||
```
|
||||
|
||||
Discard all data in the receive or transmit ring buffer.
|
||||
Discard all data in the receive or transmit ring buffer by resetting
|
||||
head and tail pointers to zero.
|
||||
|
||||
#### Getters
|
||||
### Getters
|
||||
|
||||
```c
|
||||
int rs232GetBase(int com); // UART I/O base address
|
||||
|
|
@ -141,50 +282,46 @@ int rs232GetDsr(int com); // DSR line state (0 or 1)
|
|||
int rs232GetDtr(int com); // DTR line state (0 or 1)
|
||||
int rs232GetHandshake(int com); // Handshaking mode
|
||||
int rs232GetIrq(int com); // IRQ number
|
||||
int rs232GetLsr(int com); // Line status register
|
||||
int rs232GetMcr(int com); // Modem control register
|
||||
int rs232GetMsr(int com); // Modem status register
|
||||
int rs232GetLsr(int com); // Line Status Register
|
||||
int rs232GetMcr(int com); // Modem Control Register
|
||||
int rs232GetMsr(int com); // Modem Status Register
|
||||
char rs232GetParity(int com); // Parity setting ('N','O','E','M','S')
|
||||
int rs232GetRts(int com); // RTS line state (0 or 1)
|
||||
int rs232GetRxBuffered(int com); // Bytes in receive buffer
|
||||
int rs232GetRxBuffered(int com); // Bytes waiting in RX buffer
|
||||
int rs232GetStop(int com); // Stop bits setting
|
||||
int rs232GetTxBuffered(int com); // Bytes in transmit buffer
|
||||
int rs232GetTxBuffered(int com); // Bytes waiting in TX buffer
|
||||
int rs232GetUartType(int com); // UART type (RS232_UART_* constant)
|
||||
```
|
||||
|
||||
`rs232GetUartType` probes the UART hardware to identify the chip:
|
||||
Most getters return cached register values from the per-port state
|
||||
structure, avoiding unnecessary I/O port reads. `rs232GetUartType()`
|
||||
actively probes the hardware (see UART Type Detection above).
|
||||
|
||||
1. **Scratch register test** -- writes two values to register 7 and
|
||||
reads them back. The 8250 lacks this register, so readback fails.
|
||||
2. **FIFO test** -- enables the FIFO via the FCR, then reads IIR bits
|
||||
7:6. `0b11` = 16550A (working FIFO), `0b10` = 16550 (broken FIFO),
|
||||
`0b00` = 16450 (no FIFO). The original FCR value is restored after
|
||||
probing.
|
||||
|
||||
#### Setters
|
||||
### Setters
|
||||
|
||||
```c
|
||||
int rs232Set(int com, int32_t bps, int dataBits, char parity,
|
||||
int stopBits, int handshake);
|
||||
```
|
||||
|
||||
Reconfigure all port parameters at once (port must be open).
|
||||
Reconfigure all port parameters at once. The port must already be open.
|
||||
|
||||
```c
|
||||
int rs232SetBase(int com, int base); // Override I/O base address
|
||||
int rs232SetBase(int com, int base); // Override I/O base (before open)
|
||||
int rs232SetBps(int com, int32_t bps); // Change baud rate
|
||||
int rs232SetData(int com, int dataBits); // Change data bits
|
||||
int rs232SetDtr(int com, bool dtr); // Assert/deassert DTR
|
||||
int rs232SetFifoThreshold(int com, int thr); // FIFO trigger level (1,4,8,14)
|
||||
int rs232SetFifoThreshold(int com, int thr); // FIFO trigger (1, 4, 8, 14)
|
||||
int rs232SetHandshake(int com, int handshake); // Change flow control mode
|
||||
int rs232SetIrq(int com, int irq); // Override IRQ (before Open)
|
||||
int rs232SetMcr(int com, int mcr); // Write modem control register
|
||||
int rs232SetIrq(int com, int irq); // Override IRQ (before open)
|
||||
int rs232SetMcr(int com, int mcr); // Write Modem Control Register
|
||||
int rs232SetParity(int com, char parity); // Change parity
|
||||
int rs232SetRts(int com, bool rts); // Assert/deassert RTS
|
||||
int rs232SetStop(int com, int stopBits); // Change stop bits
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
## Usage Example
|
||||
|
||||
```c
|
||||
#include "rs232.h"
|
||||
|
|
@ -196,14 +333,19 @@ int main(void) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
// Identify UART chip
|
||||
// Identify the UART chip
|
||||
int uartType = rs232GetUartType(RS232_COM1);
|
||||
// uartType == RS232_UART_16550A on most systems
|
||||
// uartType == RS232_UART_16550A on most 486+ systems
|
||||
|
||||
// Enable 16550A FIFO with trigger at 14 bytes
|
||||
if (uartType == RS232_UART_16550A) {
|
||||
rs232SetFifoThreshold(RS232_COM1, 14);
|
||||
}
|
||||
|
||||
// Blocking send
|
||||
rs232Write(RS232_COM1, "Hello\r\n", 7);
|
||||
|
||||
// Non-blocking receive
|
||||
// Non-blocking receive loop
|
||||
char buf[128];
|
||||
int n;
|
||||
while ((n = rs232Read(RS232_COM1, buf, sizeof(buf))) > 0) {
|
||||
|
|
@ -215,17 +357,38 @@ int main(void) {
|
|||
}
|
||||
```
|
||||
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The ISR handles all four COM ports from a single shared handler.
|
||||
On entry it disables UART interrupts for all open ports, then
|
||||
re-enables CPU interrupts so higher-priority devices are serviced.
|
||||
- The single shared ISR handles all four COM ports. On entry it disables
|
||||
UART interrupts for all open ports on the PIC, then re-enables CPU
|
||||
interrupts (STI) so higher-priority devices (timer, keyboard) are
|
||||
serviced promptly.
|
||||
|
||||
- Ring buffers use power-of-2 sizes (2048 bytes) with bitmask indexing
|
||||
for zero-branch wraparound.
|
||||
- Flow control watermarks are at 80% (assert) and 20% (deassert) of
|
||||
buffer capacity.
|
||||
- DPMI `__dpmi_lock_linear_region` is used to pin the ISR, ring
|
||||
buffers, and port state in physical memory.
|
||||
for zero-branch wraparound. Each port uses 4KB total (2KB RX + 2KB TX).
|
||||
|
||||
- Flow control watermarks are at 80% (assert stop) and 20% (deassert
|
||||
stop) of buffer capacity. These percentages are defined as compile-time
|
||||
constants and apply to both RX and TX directions.
|
||||
|
||||
- DPMI `__dpmi_lock_linear_region` is used to pin the ISR code, ring
|
||||
buffers, and port state in physical memory. The ISR code region is
|
||||
locked for 2048 bytes starting at the `comGeneralIsr` function address.
|
||||
|
||||
- `rs232Write()` is a blocking polled write that bypasses the TX ring
|
||||
buffer entirely. It writes directly to the UART THR register, polling
|
||||
LSR for readiness between each byte. `rs232WriteBuf()` is the
|
||||
non-blocking alternative that queues into the TX ring buffer for ISR
|
||||
draining.
|
||||
|
||||
- Per-port state is stored in a static array of `Rs232StateT` structures
|
||||
(`sComPorts[4]`). This array is locked in physical memory alongside the
|
||||
ISR code.
|
||||
|
||||
- The BIOS Data Area (real-mode address 0040:0000) is read via DJGPP's
|
||||
far pointer API (`_farpeekw`) to obtain port base addresses at runtime.
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
|
|
@ -234,4 +397,21 @@ make # builds ../lib/librs232.a
|
|||
make clean # removes objects and library
|
||||
```
|
||||
|
||||
Target: DJGPP cross-compiler, 486+ CPU.
|
||||
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
|
||||
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
|
||||
|
||||
Objects are placed in `../obj/rs232/`, the library in `../lib/`.
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
- `rs232.h` -- Public API header
|
||||
- `rs232.c` -- Complete implementation (ISR, DPMI, ring buffers, UART I/O)
|
||||
- `Makefile` -- DJGPP cross-compilation build rules
|
||||
|
||||
|
||||
## Used By
|
||||
|
||||
- `packet/` -- Packetized serial transport layer (HDLC framing, CRC, ARQ)
|
||||
- `seclink/` -- Secure serial link (opens and closes the COM port)
|
||||
- `proxy/` -- Linux serial proxy (uses a socket-based shim of this API)
|
||||
|
|
|
|||
|
|
@ -1,28 +1,39 @@
|
|||
# SecLink -- Secure Serial Link Library
|
||||
|
||||
SecLink is a convenience wrapper that ties together three lower-level
|
||||
libraries into a single API for reliable, optionally encrypted serial
|
||||
communication:
|
||||
SecLink is the top-level API for the DVX serial/networking stack. It
|
||||
composes three lower-level libraries into a single interface for reliable,
|
||||
optionally encrypted, channel-multiplexed serial communication:
|
||||
|
||||
- **rs232** -- ISR-driven UART I/O with ring buffers and flow control
|
||||
- **packet** -- HDLC-style framing with CRC-16 and sliding window reliability
|
||||
- **security** -- 1024-bit Diffie-Hellman key exchange and XTEA-CTR encryption
|
||||
- **packet** -- HDLC framing, CRC-16, Go-Back-N sliding window ARQ
|
||||
- **security** -- 1024-bit Diffie-Hellman key exchange, XTEA-CTR encryption
|
||||
|
||||
SecLink adds channel multiplexing and per-packet encryption control on
|
||||
top of the packet layer's reliable delivery.
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Application
|
||||
|
|
||||
[secLink] channels, optional encryption
|
||||
| secLinkSend() send data on a channel, optionally encrypted
|
||||
| secLinkPoll() receive, decrypt, deliver to callback
|
||||
| secLinkHandshake() DH key exchange (blocking)
|
||||
|
|
||||
[packet] framing, CRC, retransmit, ordering
|
||||
[SecLink] channel header, encrypt/decrypt, key management
|
||||
|
|
||||
[rs232] ISR-driven UART, ring buffers, flow control
|
||||
[Packet] HDLC framing, CRC-16, Go-Back-N ARQ
|
||||
|
|
||||
UART
|
||||
[RS232] ISR-driven UART, 2048-byte ring buffers
|
||||
|
|
||||
UART Hardware
|
||||
```
|
||||
|
||||
SecLink adds a one-byte header to every packet:
|
||||
### Channel Multiplexing
|
||||
|
||||
SecLink prepends a one-byte header to every packet's payload before
|
||||
handing it to the packet layer:
|
||||
|
||||
```
|
||||
Bit 7 Bits 6..0
|
||||
|
|
@ -30,47 +41,113 @@ SecLink adds a one-byte header to every packet:
|
|||
Encrypt Channel (0-127)
|
||||
```
|
||||
|
||||
This allows mixing encrypted and cleartext traffic on up to 128
|
||||
independent logical channels over a single serial link.
|
||||
This allows up to 128 independent logical channels over a single serial
|
||||
link. Each channel can carry a different type of traffic (terminal data,
|
||||
file transfer, control messages, etc.) without needing separate framing
|
||||
or sequencing per stream. The receive callback includes the channel
|
||||
number so the application can dispatch accordingly.
|
||||
|
||||
The encrypt flag (bit 7) tells the receiver whether the payload portion
|
||||
of this packet is encrypted. The channel header byte itself is always
|
||||
sent in the clear.
|
||||
|
||||
### Mixed Clear and Encrypted Traffic
|
||||
|
||||
Unencrypted packets can be sent before or after the DH handshake. This
|
||||
enables a startup protocol (version negotiation, capability exchange)
|
||||
before keys are established. Encrypted packets require a completed
|
||||
handshake -- attempting to send an encrypted packet before the handshake
|
||||
returns `SECLINK_ERR_NOT_READY`.
|
||||
|
||||
On the receive side, encrypted packets arriving before the handshake is
|
||||
complete are silently dropped. Cleartext packets are delivered regardless
|
||||
of handshake state.
|
||||
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```
|
||||
secLinkOpen() Open COM port and packet layer
|
||||
secLinkHandshake() DH key exchange (blocks until complete)
|
||||
secLinkSend() Send a packet (encrypted or clear)
|
||||
secLinkHandshake() DH key exchange (blocks until both sides complete)
|
||||
secLinkSend() Send data on a channel (encrypted or cleartext)
|
||||
secLinkPoll() Receive and deliver packets to callback
|
||||
secLinkClose() Tear down everything
|
||||
secLinkClose() Tear down everything (ciphers, packet, COM port)
|
||||
```
|
||||
|
||||
The handshake is only required if you intend to send encrypted packets.
|
||||
Cleartext packets can be sent immediately after `secLinkOpen()`.
|
||||
### Handshake Protocol
|
||||
|
||||
The DH key exchange uses the packet layer's reliable delivery, so lost
|
||||
packets are automatically retransmitted. Both sides can send their public
|
||||
key simultaneously -- there is no initiator/responder distinction.
|
||||
|
||||
1. Both sides generate a DH keypair (256-bit private, 1024-bit public)
|
||||
2. Both sides send their 128-byte public key as a single packet
|
||||
3. On receiving the remote's public key, each side immediately computes
|
||||
the shared secret (`remote^private mod p`)
|
||||
4. Each side derives separate TX and RX cipher keys from the master key
|
||||
5. Cipher contexts are created and the link transitions to READY state
|
||||
6. The DH context (containing the private key) is destroyed immediately
|
||||
|
||||
**Directional key derivation:**
|
||||
|
||||
The side with the lexicographically lower public key uses
|
||||
`masterKey XOR 0xAA` for TX and `masterKey XOR 0x55` for RX. The other
|
||||
side uses the reverse assignment. This is critical for CTR mode security:
|
||||
if both sides used the same key and counter, they would produce identical
|
||||
keystreams, and XORing two ciphertexts would reveal the XOR of the
|
||||
plaintexts. The XOR-derived directional keys ensure each direction has a
|
||||
unique keystream even though both sides start their counters at zero.
|
||||
|
||||
**Forward secrecy:**
|
||||
|
||||
The DH context (containing the private key and shared secret) is
|
||||
destroyed immediately after deriving the session cipher keys. Even if
|
||||
the application's long-term state is compromised later, past session
|
||||
keys cannot be recovered from memory.
|
||||
|
||||
|
||||
## Payload Size
|
||||
|
||||
The maximum payload per `secLinkSend()` call is `SECLINK_MAX_PAYLOAD`
|
||||
(254 bytes). This is the packet layer's 255-byte maximum minus the
|
||||
1-byte channel header that SecLink prepends.
|
||||
|
||||
For sending data larger than 254 bytes, use `secLinkSendBuf()` which
|
||||
automatically splits the data into 254-byte chunks and sends each one
|
||||
with blocking delivery.
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
### Types
|
||||
|
||||
```c
|
||||
// Receive callback -- called for each incoming packet with plaintext
|
||||
typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel);
|
||||
// Receive callback -- delivers plaintext with channel number
|
||||
typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len,
|
||||
uint8_t channel);
|
||||
|
||||
// Opaque connection handle
|
||||
typedef struct SecLinkS SecLinkT;
|
||||
```
|
||||
|
||||
The receive callback is invoked from `secLinkPoll()` for each incoming
|
||||
packet. Encrypted packets are decrypted before delivery -- the callback
|
||||
always receives plaintext regardless of whether encryption was used on
|
||||
the wire. The `data` pointer is valid only during the callback.
|
||||
|
||||
### Constants
|
||||
|
||||
| Name | Value | Description |
|
||||
|-------------------------|-------|----------------------------------------|
|
||||
|-------------------------|-------|---------------------------------------|
|
||||
| `SECLINK_MAX_PAYLOAD` | 254 | Max bytes per `secLinkSend()` call |
|
||||
| `SECLINK_MAX_CHANNEL` | 127 | Highest valid channel number |
|
||||
| `SECLINK_SUCCESS` | 0 | Operation succeeded |
|
||||
| `SECLINK_ERR_PARAM` | -1 | Invalid parameter |
|
||||
| `SECLINK_ERR_SERIAL` | -2 | Serial port error |
|
||||
| `SECLINK_ERR_PARAM` | -1 | Invalid parameter or NULL pointer |
|
||||
| `SECLINK_ERR_SERIAL` | -2 | Serial port open failed |
|
||||
| `SECLINK_ERR_ALLOC` | -3 | Memory allocation failed |
|
||||
| `SECLINK_ERR_HANDSHAKE` | -4 | Key exchange failed |
|
||||
| `SECLINK_ERR_HANDSHAKE` | -4 | DH key exchange failed |
|
||||
| `SECLINK_ERR_NOT_READY` | -5 | Encryption requested before handshake |
|
||||
| `SECLINK_ERR_SEND` | -6 | Packet layer send failed |
|
||||
| `SECLINK_ERR_SEND` | -6 | Packet layer send failed or window full |
|
||||
|
||||
### Functions
|
||||
|
||||
|
|
@ -82,9 +159,22 @@ SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity,
|
|||
SecLinkRecvT callback, void *ctx);
|
||||
```
|
||||
|
||||
Opens the COM port via rs232, creates the packet layer, and returns a
|
||||
link handle. Returns `NULL` on failure. The callback is invoked from
|
||||
`secLinkPoll()` for each received packet.
|
||||
Opens the COM port via rs232, creates the packet layer with default
|
||||
window size (4), and returns a link handle. The callback is invoked from
|
||||
`secLinkPoll()` for each received packet (decrypted if applicable).
|
||||
|
||||
Returns `NULL` on failure (serial port error, packet layer allocation
|
||||
error, or memory allocation failure). On failure, all partially
|
||||
initialized resources are cleaned up.
|
||||
|
||||
- `com` -- RS232 port index (`RS232_COM1` through `RS232_COM4`)
|
||||
- `bps` -- baud rate (50 through 115200)
|
||||
- `dataBits` -- 5, 6, 7, or 8
|
||||
- `parity` -- `'N'`, `'O'`, `'E'`, `'M'`, or `'S'`
|
||||
- `stopBits` -- 1 or 2
|
||||
- `handshake` -- `RS232_HANDSHAKE_*` constant
|
||||
- `callback` -- receive callback function
|
||||
- `ctx` -- user pointer passed through to the callback
|
||||
|
||||
#### secLinkClose
|
||||
|
||||
|
|
@ -92,8 +182,9 @@ link handle. Returns `NULL` on failure. The callback is invoked from
|
|||
void secLinkClose(SecLinkT *link);
|
||||
```
|
||||
|
||||
Destroys cipher contexts, closes the packet layer and COM port, and
|
||||
frees all memory.
|
||||
Full teardown in order: destroys TX and RX cipher contexts (secure zero),
|
||||
destroys the DH context if still present, closes the packet layer, closes
|
||||
the COM port, zeroes the link structure, and frees memory.
|
||||
|
||||
#### secLinkHandshake
|
||||
|
||||
|
|
@ -105,41 +196,17 @@ Performs a Diffie-Hellman key exchange. Blocks until both sides have
|
|||
exchanged public keys and derived cipher keys. The RNG must be seeded
|
||||
(via `secRngSeed()` or `secRngAddEntropy()`) before calling this.
|
||||
|
||||
Each side derives separate TX and RX keys from the shared secret,
|
||||
using public key ordering to determine directionality. This prevents
|
||||
CTR counter collisions.
|
||||
Internally:
|
||||
1. Creates a DH context and generates keys
|
||||
2. Sends the 128-byte public key via the packet layer (blocking)
|
||||
3. Polls the packet layer in a loop until the remote's public key arrives
|
||||
4. Computes the shared secret and derives directional cipher keys
|
||||
5. Destroys the DH context (forward secrecy)
|
||||
6. Transitions the link to READY state
|
||||
|
||||
#### secLinkGetPending
|
||||
|
||||
```c
|
||||
int secLinkGetPending(SecLinkT *link);
|
||||
```
|
||||
|
||||
Returns the number of unacknowledged packets in the transmit window.
|
||||
Useful for non-blocking send loops to determine if there is room to
|
||||
send more data.
|
||||
|
||||
#### secLinkIsReady
|
||||
|
||||
```c
|
||||
bool secLinkIsReady(SecLinkT *link);
|
||||
```
|
||||
|
||||
Returns `true` if the handshake is complete and the link is ready for
|
||||
encrypted communication.
|
||||
|
||||
#### secLinkPoll
|
||||
|
||||
```c
|
||||
int secLinkPoll(SecLinkT *link);
|
||||
```
|
||||
|
||||
Reads available serial data, processes received frames, handles ACKs
|
||||
and retransmits. Decrypts encrypted packets and delivers plaintext to
|
||||
the receive callback. Returns the number of packets delivered, or
|
||||
negative on error.
|
||||
|
||||
Must be called frequently (e.g. in your main loop).
|
||||
Returns `SECLINK_SUCCESS` or `SECLINK_ERR_HANDSHAKE` on failure
|
||||
(DH key generation failure, send failure, or serial disconnect during
|
||||
the exchange).
|
||||
|
||||
#### secLinkSend
|
||||
|
||||
|
|
@ -148,14 +215,24 @@ int secLinkSend(SecLinkT *link, const uint8_t *data, int len,
|
|||
uint8_t channel, bool encrypt, bool block);
|
||||
```
|
||||
|
||||
Sends up to `SECLINK_MAX_PAYLOAD` (255) bytes on the given channel.
|
||||
Sends up to `SECLINK_MAX_PAYLOAD` (254) bytes on the given channel.
|
||||
|
||||
- `channel` -- logical channel number (0-127)
|
||||
- `encrypt` -- if `true`, encrypts the payload (requires completed handshake)
|
||||
- `block` -- if `true`, waits for transmit window space; if `false`,
|
||||
returns `SECLINK_ERR_SEND` when the window is full
|
||||
- `encrypt` -- if `true`, encrypts the payload before sending. Requires
|
||||
a completed handshake; returns `SECLINK_ERR_NOT_READY` otherwise.
|
||||
- `block` -- if `true`, waits for transmit window space. If `false`,
|
||||
returns `SECLINK_ERR_SEND` when the packet layer's window is full.
|
||||
|
||||
Cleartext packets (`encrypt = false`) can be sent before the handshake.
|
||||
**Cipher counter safety:** The function checks transmit window space
|
||||
BEFORE encrypting the payload. If it encrypted first and then the send
|
||||
failed, the cipher counter would advance without the data being sent,
|
||||
permanently desynchronizing the TX cipher state from the remote's RX
|
||||
cipher. This ordering is critical for correctness.
|
||||
|
||||
The channel header byte is prepended to the data, and only the payload
|
||||
portion (not the header) is encrypted.
|
||||
|
||||
Returns `SECLINK_SUCCESS` or an error code.
|
||||
|
||||
#### secLinkSendBuf
|
||||
|
||||
|
|
@ -165,12 +242,54 @@ int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len,
|
|||
```
|
||||
|
||||
Sends an arbitrarily large buffer by splitting it into
|
||||
`SECLINK_MAX_PAYLOAD`-byte chunks. Always blocks until all data is
|
||||
sent. Returns `SECLINK_SUCCESS` or the first error encountered.
|
||||
`SECLINK_MAX_PAYLOAD`-byte (254-byte) chunks. Always blocks until all
|
||||
data is sent. The receiver sees multiple packets on the same channel and
|
||||
must reassemble if needed.
|
||||
|
||||
## Examples
|
||||
Returns `SECLINK_SUCCESS` or the first error encountered.
|
||||
|
||||
### Basic encrypted link
|
||||
#### secLinkPoll
|
||||
|
||||
```c
|
||||
int secLinkPoll(SecLinkT *link);
|
||||
```
|
||||
|
||||
Delegates to `pktPoll()` to read serial data, process frames, handle
|
||||
ACKs and retransmits. Received packets are routed through an internal
|
||||
callback that:
|
||||
|
||||
- During handshake: expects a 128-byte DH public key
|
||||
- When ready: strips the channel header, decrypts the payload if the
|
||||
encrypt flag is set, and forwards plaintext to the user callback
|
||||
|
||||
Returns the number of packets delivered, or a negative error code.
|
||||
|
||||
Must be called frequently (every iteration of your main loop).
|
||||
|
||||
#### secLinkGetPending
|
||||
|
||||
```c
|
||||
int secLinkGetPending(SecLinkT *link);
|
||||
```
|
||||
|
||||
Returns the number of unacknowledged packets in the transmit window.
|
||||
Delegates directly to `pktGetPending()`. Useful for non-blocking send
|
||||
loops to determine when there is room to send more data.
|
||||
|
||||
#### secLinkIsReady
|
||||
|
||||
```c
|
||||
bool secLinkIsReady(SecLinkT *link);
|
||||
```
|
||||
|
||||
Returns `true` if the DH handshake is complete and the link is ready for
|
||||
encrypted communication. Cleartext sends do not require the link to be
|
||||
ready.
|
||||
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Encrypted Link
|
||||
|
||||
```c
|
||||
#include "secLink.h"
|
||||
|
|
@ -186,13 +305,14 @@ int main(void) {
|
|||
secRngGatherEntropy(entropy, sizeof(entropy));
|
||||
secRngSeed(entropy, sizeof(entropy));
|
||||
|
||||
// Open link on COM1 at 115200 8N1
|
||||
SecLinkT *link = secLinkOpen(0, 115200, 8, 'N', 1, 0, onRecv, NULL);
|
||||
// Open link on COM1 at 115200 8N1, no flow control
|
||||
SecLinkT *link = secLinkOpen(RS232_COM1, 115200, 8, 'N', 1,
|
||||
RS232_HANDSHAKE_NONE, onRecv, NULL);
|
||||
if (!link) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Key exchange (blocks until both sides complete)
|
||||
// DH key exchange (blocks until both sides complete)
|
||||
if (secLinkHandshake(link) != SECLINK_SUCCESS) {
|
||||
secLinkClose(link);
|
||||
return 1;
|
||||
|
|
@ -212,31 +332,31 @@ int main(void) {
|
|||
}
|
||||
```
|
||||
|
||||
### Mixed encrypted and cleartext channels
|
||||
### Mixed Encrypted and Cleartext Channels
|
||||
|
||||
```c
|
||||
#define CHAN_CONTROL 0 // cleartext control channel
|
||||
#define CHAN_DATA 1 // encrypted data channel
|
||||
|
||||
// Send a cleartext status message (no handshake needed)
|
||||
// Cleartext status message (no handshake needed)
|
||||
secLinkSend(link, statusMsg, statusLen, CHAN_CONTROL, false, true);
|
||||
|
||||
// Send encrypted payload (requires completed handshake)
|
||||
// Encrypted payload (requires completed handshake)
|
||||
secLinkSend(link, payload, payloadLen, CHAN_DATA, true, true);
|
||||
```
|
||||
|
||||
### Non-blocking file transfer
|
||||
### Non-Blocking File Transfer
|
||||
|
||||
```c
|
||||
int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize,
|
||||
uint8_t channel, bool encrypt, int windowSize) {
|
||||
uint8_t channel, bool encrypt) {
|
||||
int offset = 0;
|
||||
int bytesLeft = fileSize;
|
||||
|
||||
while (bytesLeft > 0) {
|
||||
secLinkPoll(link); // process ACKs, free window slots
|
||||
|
||||
if (secLinkGetPending(link) < windowSize) {
|
||||
if (secLinkGetPending(link) < 4) { // window has room
|
||||
int chunk = bytesLeft;
|
||||
if (chunk > SECLINK_MAX_PAYLOAD) {
|
||||
chunk = SECLINK_MAX_PAYLOAD;
|
||||
|
|
@ -248,7 +368,7 @@ int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize,
|
|||
offset += chunk;
|
||||
bytesLeft -= chunk;
|
||||
}
|
||||
// SECLINK_ERR_SEND means window full, just retry next iteration
|
||||
// SECLINK_ERR_SEND means window full, retry next iteration
|
||||
}
|
||||
|
||||
// Application can do other work here:
|
||||
|
|
@ -264,13 +384,34 @@ int sendFile(SecLinkT *link, const uint8_t *fileData, int fileSize,
|
|||
}
|
||||
```
|
||||
|
||||
### Blocking bulk transfer
|
||||
### Blocking Bulk Transfer
|
||||
|
||||
```c
|
||||
// Send an entire file in one call (blocks until complete)
|
||||
secLinkSendBuf(link, fileData, fileSize, CHAN_DATA, true);
|
||||
```
|
||||
|
||||
|
||||
## Internal State Machine
|
||||
|
||||
SecLink maintains a three-state internal state machine:
|
||||
|
||||
| State | Value | Description |
|
||||
|--------------|-------|----------------------------------------------|
|
||||
| `STATE_INIT` | 0 | Link open, no handshake attempted yet |
|
||||
| `STATE_HANDSHAKE` | 1 | DH key exchange in progress |
|
||||
| `STATE_READY` | 2 | Handshake complete, ciphers ready |
|
||||
|
||||
Transitions:
|
||||
- `INIT -> HANDSHAKE`: when `secLinkHandshake()` is called
|
||||
- `HANDSHAKE -> READY`: when the remote's public key is received and
|
||||
cipher keys are derived
|
||||
- Any state -> cleanup: when `secLinkClose()` is called
|
||||
|
||||
Cleartext packets can be sent and received in any state. Encrypted
|
||||
packets require `STATE_READY`.
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
|
|
@ -278,18 +419,32 @@ make # builds ../lib/libseclink.a
|
|||
make clean # removes objects and library
|
||||
```
|
||||
|
||||
Link against all four libraries:
|
||||
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
|
||||
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
|
||||
|
||||
Objects are placed in `../obj/seclink/`, the library in `../lib/`.
|
||||
|
||||
Link against all four libraries in this order:
|
||||
|
||||
```
|
||||
-lseclink -lpacket -lsecurity -lrs232
|
||||
```
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
- `secLink.h` -- Public API header (types, constants, function prototypes)
|
||||
- `secLink.c` -- Complete implementation (handshake, send, receive, state
|
||||
machine)
|
||||
- `Makefile` -- DJGPP cross-compilation build rules
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
SecLink requires these libraries (all in `../lib/`):
|
||||
SecLink requires these libraries (all built into `../lib/`):
|
||||
|
||||
- `librs232.a` -- serial port driver
|
||||
- `libpacket.a` -- packet framing and reliability
|
||||
- `libsecurity.a` -- DH key exchange and XTEA cipher
|
||||
|
||||
Target: DJGPP cross-compiler, 486+ CPU.
|
||||
| Library | Purpose |
|
||||
|------------------|---------------------------------------------|
|
||||
| `librs232.a` | Serial port driver (ISR, ring buffers) |
|
||||
| `libpacket.a` | HDLC framing, CRC-16, Go-Back-N ARQ |
|
||||
| `libsecurity.a` | DH key exchange, XTEA-CTR cipher, RNG |
|
||||
|
|
|
|||
|
|
@ -1,55 +1,222 @@
|
|||
# Security -- DH Key Exchange and XTEA-CTR Cipher
|
||||
# Security -- Diffie-Hellman Key Exchange and XTEA-CTR Cipher
|
||||
|
||||
Cryptographic library providing Diffie-Hellman key exchange, XTEA
|
||||
symmetric encryption in CTR mode, and a DRBG-based pseudo-random number
|
||||
generator. Optimized for 486-class DOS hardware running under DJGPP/DPMI.
|
||||
|
||||
This library has no dependencies on the serial stack and can be used
|
||||
independently for any application requiring key exchange, encryption,
|
||||
or random number generation.
|
||||
|
||||
Cryptographic library providing Diffie-Hellman key exchange and XTEA
|
||||
symmetric encryption, optimized for 486-class DOS hardware running
|
||||
under DJGPP/DPMI.
|
||||
|
||||
## Components
|
||||
|
||||
### Diffie-Hellman Key Exchange
|
||||
### 1. XTEA Cipher (CTR Mode)
|
||||
|
||||
- 1024-bit MODP group (RFC 2409 Group 2 safe prime)
|
||||
- 256-bit private exponents for fast computation on 486 CPUs
|
||||
- Montgomery multiplication (CIOS variant) for modular exponentiation
|
||||
- Lazy-initialized Montgomery constants (R^2 mod p, -p0^-1 mod 2^32)
|
||||
XTEA (eXtended Tiny Encryption Algorithm) is a 64-bit block cipher with a
|
||||
128-bit key and 32 Feistel rounds. In CTR (counter) mode, it operates as
|
||||
a stream cipher: an incrementing counter is encrypted with the key to
|
||||
produce a keystream, which is XOR'd with the plaintext. Because XOR is
|
||||
its own inverse, the same operation encrypts and decrypts.
|
||||
|
||||
### XTEA Cipher (CTR Mode)
|
||||
**Why XTEA instead of AES or DES:**
|
||||
|
||||
- 128-bit key, 64-bit block size, 32 rounds
|
||||
- CTR mode -- encrypt and decrypt are the same XOR operation
|
||||
- No lookup tables, no key schedule -- just shifts, adds, and XORs
|
||||
- Ideal for constrained environments with small key setup cost
|
||||
XTEA requires zero lookup tables, no key schedule, and compiles to
|
||||
approximately 20 instructions per round (shifts, adds, and XORs only).
|
||||
This makes it ideal for a 486 where the data cache is tiny (8KB) and
|
||||
AES's 4KB S-boxes would thrash it. DES is similarly table-heavy and has
|
||||
a complex key schedule. XTEA has no library dependencies -- the entire
|
||||
cipher fits in about a dozen lines of C. At 32 rounds, XTEA provides
|
||||
128-bit security with negligible per-byte overhead even on the slowest
|
||||
target hardware.
|
||||
|
||||
### Pseudo-Random Number Generator
|
||||
**CTR mode properties:**
|
||||
|
||||
- Encrypt and decrypt are the same function (XOR is symmetric)
|
||||
- No padding required -- operates on arbitrary-length data
|
||||
- Random access possible (set the counter to any value)
|
||||
- CRITICAL: the same counter value must never be reused with the same key.
|
||||
Reuse reveals the XOR of two plaintexts. The secLink layer prevents this
|
||||
by deriving separate TX/RX cipher keys for each direction.
|
||||
|
||||
**XTEA block cipher internals:**
|
||||
|
||||
The Feistel network uses the golden-ratio constant (delta = 0x9E3779B9)
|
||||
as a round key mixer. Each round combines the two 32-bit halves using
|
||||
shifts, additions, and XORs. The delta ensures each round uses a
|
||||
different effective subkey, preventing slide attacks. No S-boxes or lookup
|
||||
tables are involved anywhere in the computation.
|
||||
|
||||
### 2. Diffie-Hellman Key Exchange (1024-bit)
|
||||
|
||||
Uses the RFC 2409 Group 2 safe prime (1024-bit MODP group) with a
|
||||
generator of 2. Private exponents are 256 bits for fast computation on
|
||||
486-class hardware.
|
||||
|
||||
**Why 1024-bit DH with 256-bit private exponents:**
|
||||
|
||||
RFC 2409 Group 2 provides a well-audited, interoperable safe prime.
|
||||
256-bit private exponents (versus full 1024-bit) reduce the modular
|
||||
exponentiation from approximately 1024 squarings+multiplies to approximately
|
||||
256 squarings + approximately 128 multiplies (half the exponent bits are 1 on
|
||||
average). This makes key generation feasible on a 486 in under a second
|
||||
rather than minutes. The security reduction is negligible -- Pollard's
|
||||
rho on a 256-bit exponent requires approximately 2^128 operations, matching
|
||||
XTEA's key strength.
|
||||
|
||||
**Key validation:**
|
||||
|
||||
`secDhComputeSecret()` validates that the remote public key is in the
|
||||
range [2, p-2] to prevent small-subgroup attacks. Keys of 0, 1, or p-1
|
||||
would produce trivially guessable shared secrets.
|
||||
|
||||
**Key derivation:**
|
||||
|
||||
The 128-byte shared secret is reduced to a symmetric key via XOR-folding:
|
||||
each byte of the secret is XOR'd into the output key at position
|
||||
`i % keyLen`. For a 16-byte XTEA key, each output byte is the XOR of
|
||||
8 secret bytes, providing thorough mixing. A proper KDF (HKDF, etc.)
|
||||
would be more rigorous but adds complexity and code size for marginal
|
||||
benefit in this use case.
|
||||
|
||||
### 3. Pseudo-Random Number Generator
|
||||
|
||||
XTEA-CTR based DRBG (Deterministic Random Bit Generator). The RNG
|
||||
encrypts a monotonically increasing 64-bit counter with a 128-bit XTEA
|
||||
key, producing 8 bytes of pseudorandom output per block. The counter
|
||||
never repeats (64-bit space is sufficient for any practical session
|
||||
length), so the output is a pseudorandom stream as long as the key has
|
||||
sufficient entropy.
|
||||
|
||||
**Hardware entropy sources:**
|
||||
|
||||
- PIT (Programmable Interval Timer) -- runs at 1.193182 MHz. Its LSBs
|
||||
change rapidly and provide approximately 10 bits of entropy per read,
|
||||
depending on timing jitter. Two readings with intervening code execution
|
||||
provide additional jitter.
|
||||
- BIOS tick count -- 18.2 Hz timer at real-mode address 0040:046C. Adds
|
||||
a few more bits of entropy.
|
||||
|
||||
Total from hardware: roughly 20 bits of real entropy per call to
|
||||
`secRngGatherEntropy()`. This is not enough on its own for
|
||||
cryptographic use but is sufficient to seed the DRBG when supplemented
|
||||
by user interaction timing (keyboard, mouse jitter).
|
||||
|
||||
**Seeding and mixing:**
|
||||
|
||||
The seed function (`secRngSeed()`) XOR-folds the entropy into the XTEA
|
||||
key, derives the initial counter from the key bits, and then generates and
|
||||
discards 64 bytes to advance past any weak initial output. This discard
|
||||
step is standard DRBG practice -- it ensures the first bytes the caller
|
||||
receives do not leak information about the seed material.
|
||||
|
||||
Additional entropy can be stirred in at any time via `secRngAddEntropy()`
|
||||
without resetting the RNG state. This function XOR-folds new entropy into
|
||||
the key and then re-mixes by encrypting the key with itself, diffusing
|
||||
the new entropy across all key bits.
|
||||
|
||||
Auto-seeding: if `secRngBytes()` is called before `secRngSeed()`, it
|
||||
automatically gathers hardware entropy and seeds itself as a safety net.
|
||||
|
||||
|
||||
## BigNum Arithmetic
|
||||
|
||||
All modular arithmetic uses a 1024-bit big number type (`BigNumT`)
|
||||
stored as 32 x `uint32_t` words in little-endian order. Operations:
|
||||
|
||||
| Function | Description |
|
||||
|----------------|------------------------------------------------------|
|
||||
| `bnAdd` | Add two bignums, return carry |
|
||||
| `bnSub` | Subtract two bignums, return borrow |
|
||||
| `bnCmp` | Compare two bignums (-1, 0, +1) |
|
||||
| `bnBit` | Test a single bit by index |
|
||||
| `bnBitLength` | Find the highest set bit position |
|
||||
| `bnShiftLeft1` | Left-shift by 1, return carry |
|
||||
| `bnClear` | Zero all words |
|
||||
| `bnSet` | Set to a 32-bit value (clear upper words) |
|
||||
| `bnCopy` | Copy from source to destination |
|
||||
| `bnFromBytes` | Convert big-endian byte array to little-endian words |
|
||||
| `bnToBytes` | Convert little-endian words to big-endian byte array |
|
||||
| `bnMontMul` | Montgomery multiplication (CIOS variant) |
|
||||
| `bnModExp` | Modular exponentiation via Montgomery multiply |
|
||||
|
||||
|
||||
## Montgomery Multiplication
|
||||
|
||||
The CIOS (Coarsely Integrated Operand Scanning) variant computes
|
||||
`a * b * R^(-1) mod m` in a single pass without explicit division by the
|
||||
modulus. This replaces the expensive modular reduction step (division by a
|
||||
1024-bit number) with cheaper additions and right-shifts.
|
||||
|
||||
For each of the 32 outer iterations (one per word of operand `a`):
|
||||
1. Accumulate `a[i] * b` into the temporary product `t`
|
||||
2. Compute the Montgomery reduction factor `u = t[0] * m0inv mod 2^32`
|
||||
3. Add `u * mod` to `t` and shift right by 32 bits (implicit division)
|
||||
|
||||
After all iterations, the result is in the range [0, 2m), so a single
|
||||
conditional subtraction brings it into [0, m).
|
||||
|
||||
**Montgomery constants** (computed once, lazily on first DH use):
|
||||
|
||||
- `R^2 mod p` -- computed via 2048 iterations of shift-left-1 with
|
||||
conditional subtraction. This is the Montgomery domain conversion
|
||||
factor.
|
||||
- `-p[0]^(-1) mod 2^32` -- computed via Newton's method (5 iterations,
|
||||
doubling precision each step: 1->2->4->8->16->32 correct bits). This
|
||||
is the Montgomery reduction constant.
|
||||
|
||||
**Modular exponentiation** uses left-to-right binary square-and-multiply
|
||||
scanning. For a 256-bit private exponent, this requires approximately 256
|
||||
squarings plus approximately 128 multiplies (half the bits are 1 on average),
|
||||
where each operation is a Montgomery multiplication on 32-word numbers.
|
||||
|
||||
|
||||
## Secure Zeroing
|
||||
|
||||
Key material (private keys, shared secrets, cipher contexts) is erased
|
||||
using a volatile-pointer loop:
|
||||
|
||||
```c
|
||||
static void secureZero(void *ptr, int len) {
|
||||
volatile uint8_t *p = (volatile uint8_t *)ptr;
|
||||
for (int i = 0; i < len; i++) {
|
||||
p[i] = 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `volatile` qualifier prevents the compiler from optimizing away the
|
||||
zeroing as a dead store. Without it, the compiler would see that the
|
||||
buffer is about to be freed and remove the memset entirely. This is
|
||||
critical for preventing sensitive key material from lingering in freed
|
||||
memory where a later `malloc` could expose it.
|
||||
|
||||
- XTEA-CTR based DRBG (deterministic random bit generator)
|
||||
- Hardware entropy from PIT counter (~10 bits) and BIOS tick count
|
||||
- Supports additional entropy injection (keyboard timing, mouse, etc.)
|
||||
- Auto-seeds from hardware on first use if not explicitly seeded
|
||||
|
||||
## Performance
|
||||
|
||||
At serial port speeds, encryption overhead is minimal:
|
||||
At serial port speeds, XTEA-CTR encryption overhead is minimal:
|
||||
|
||||
| Speed | Blocks/sec | CPU cycles/sec | % of 33 MHz 486 |
|
||||
| Speed | Blocks/sec | CPU Cycles/sec | % of 33 MHz 486 |
|
||||
|----------|------------|----------------|------------------|
|
||||
| 9600 | 120 | ~240K | < 1% |
|
||||
| 57600 | 720 | ~1.4M | ~4% |
|
||||
| 115200 | 1440 | ~2.9M | ~9% |
|
||||
|
||||
DH key exchange takes approximately 0.3s at 66 MHz or 0.6s at 33 MHz
|
||||
(256-bit private exponent, 1024-bit modulus).
|
||||
DH key exchange takes approximately 0.3 seconds at 66 MHz or 0.6 seconds
|
||||
at 33 MHz (256-bit private exponent, 1024-bit modulus, Montgomery
|
||||
multiplication).
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
### Constants
|
||||
|
||||
| Name | Value | Description |
|
||||
|---------------------|-------|--------------------------------|
|
||||
| `SEC_DH_KEY_SIZE` | 128 | DH public key size (bytes) |
|
||||
| `SEC_XTEA_KEY_SIZE` | 16 | XTEA key size (bytes) |
|
||||
|---------------------|-------|-----------------------------------|
|
||||
| `SEC_DH_KEY_SIZE` | 128 | DH public key size in bytes |
|
||||
| `SEC_XTEA_KEY_SIZE` | 16 | XTEA key size in bytes |
|
||||
| `SEC_SUCCESS` | 0 | Success |
|
||||
| `SEC_ERR_PARAM` | -1 | Invalid parameter |
|
||||
| `SEC_ERR_PARAM` | -1 | Invalid parameter or NULL pointer |
|
||||
| `SEC_ERR_NOT_READY` | -2 | Keys not yet generated/derived |
|
||||
| `SEC_ERR_ALLOC` | -3 | Memory allocation failed |
|
||||
|
||||
|
|
@ -66,31 +233,35 @@ typedef struct SecCipherS SecCipherT; // Opaque cipher context
|
|||
int secRngGatherEntropy(uint8_t *buf, int len);
|
||||
```
|
||||
|
||||
Reads hardware entropy sources (PIT counter, BIOS tick count). Returns
|
||||
the number of bytes written. Provides roughly 20 bits of true entropy.
|
||||
Reads hardware entropy from the PIT counter and BIOS tick count. Returns
|
||||
the number of bytes written (up to 8). Provides roughly 20 bits of true
|
||||
entropy -- not sufficient alone, but enough to seed the DRBG when
|
||||
supplemented by user interaction timing.
|
||||
|
||||
```c
|
||||
void secRngSeed(const uint8_t *entropy, int len);
|
||||
```
|
||||
|
||||
Initializes the DRBG with the given entropy. XOR-folds the input into
|
||||
the XTEA key, derives the counter, and mixes state by generating and
|
||||
discarding 64 bytes.
|
||||
the XTEA key, derives the initial counter, and generates and discards 64
|
||||
bytes to advance past weak initial output.
|
||||
|
||||
```c
|
||||
void secRngAddEntropy(const uint8_t *data, int len);
|
||||
```
|
||||
|
||||
Mixes additional entropy into the running RNG state without resetting
|
||||
it. Use this to stir in keyboard timing, mouse jitter, or other
|
||||
runtime entropy.
|
||||
Mixes additional entropy into the running RNG state without resetting it.
|
||||
XOR-folds data into the key and re-mixes by encrypting the key with
|
||||
itself. Use this to stir in keyboard timing, mouse jitter, or other
|
||||
runtime entropy sources.
|
||||
|
||||
```c
|
||||
void secRngBytes(uint8_t *buf, int len);
|
||||
```
|
||||
|
||||
Generates `len` pseudo-random bytes. Auto-seeds from hardware if not
|
||||
previously seeded.
|
||||
Generates `len` pseudorandom bytes. Auto-seeds from hardware entropy if
|
||||
not previously seeded. Produces 8 bytes per XTEA block encryption of the
|
||||
internal counter.
|
||||
|
||||
### Diffie-Hellman Functions
|
||||
|
||||
|
|
@ -98,42 +269,48 @@ previously seeded.
|
|||
SecDhT *secDhCreate(void);
|
||||
```
|
||||
|
||||
Allocates a new DH context. Returns `NULL` on allocation failure.
|
||||
Allocates a new DH context. Returns `NULL` on allocation failure. The
|
||||
context must be destroyed with `secDhDestroy()` when no longer needed.
|
||||
|
||||
```c
|
||||
int secDhGenerateKeys(SecDhT *dh);
|
||||
```
|
||||
|
||||
Generates a 256-bit random private key and computes the corresponding
|
||||
1024-bit public key (g^private mod p). The RNG should be seeded first.
|
||||
1024-bit public key (`g^private mod p`). Lazily initializes Montgomery
|
||||
constants on first call. The RNG should be seeded before calling this.
|
||||
|
||||
```c
|
||||
int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
|
||||
```
|
||||
|
||||
Exports the public key into `buf`. On entry, `*len` must be at least
|
||||
`SEC_DH_KEY_SIZE` (128). On return, `*len` is set to 128.
|
||||
Exports the public key as a big-endian byte array into `buf`. On entry,
|
||||
`*len` must be at least `SEC_DH_KEY_SIZE` (128). On return, `*len` is
|
||||
set to 128.
|
||||
|
||||
```c
|
||||
int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
|
||||
```
|
||||
|
||||
Computes the shared secret from the remote side's public key.
|
||||
Validates that the remote key is in range [2, p-2] to prevent
|
||||
small-subgroup attacks.
|
||||
Computes the shared secret from the remote side's public key
|
||||
(`remote^private mod p`). Validates the remote key is in range [2, p-2].
|
||||
Both sides compute this independently and arrive at the same value.
|
||||
|
||||
```c
|
||||
int secDhDeriveKey(SecDhT *dh, uint8_t *key, int keyLen);
|
||||
```
|
||||
|
||||
Derives a symmetric key by XOR-folding the 128-byte shared secret
|
||||
down to `keyLen` bytes.
|
||||
Derives a symmetric key by XOR-folding the 128-byte shared secret down
|
||||
to `keyLen` bytes. Each output byte is the XOR of `128/keyLen` input
|
||||
bytes.
|
||||
|
||||
```c
|
||||
void secDhDestroy(SecDhT *dh);
|
||||
```
|
||||
|
||||
Securely zeroes and frees the DH context (private key, shared secret).
|
||||
Securely zeroes the entire DH context (private key, shared secret, public
|
||||
key) and frees the memory. Must be called to prevent key material from
|
||||
lingering in the heap.
|
||||
|
||||
### Cipher Functions
|
||||
|
||||
|
|
@ -141,30 +318,37 @@ Securely zeroes and frees the DH context (private key, shared secret).
|
|||
SecCipherT *secCipherCreate(const uint8_t *key);
|
||||
```
|
||||
|
||||
Creates an XTEA-CTR cipher context with the given 16-byte key. Counter
|
||||
starts at zero.
|
||||
Creates an XTEA-CTR cipher context with the given 16-byte key. The
|
||||
internal counter starts at zero. Returns `NULL` on allocation failure or
|
||||
NULL key.
|
||||
|
||||
```c
|
||||
void secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
|
||||
```
|
||||
|
||||
Encrypts or decrypts `data` in place. CTR mode is symmetric -- the
|
||||
same operation encrypts and decrypts.
|
||||
Encrypts or decrypts `data` in place. CTR mode is symmetric -- the same
|
||||
function handles both directions. The internal counter advances by one
|
||||
for every 8 bytes processed (one XTEA block). The counter must never
|
||||
repeat with the same key; callers are responsible for ensuring this
|
||||
(secLink handles it by using separate cipher instances per direction).
|
||||
|
||||
```c
|
||||
void secCipherSetNonce(SecCipherT *c, uint32_t nonceLo, uint32_t nonceHi);
|
||||
```
|
||||
|
||||
Sets the 64-bit nonce/counter. Call before encrypting if you need a
|
||||
specific starting counter value.
|
||||
Sets the 64-bit nonce/counter to a specific value. Both the nonce
|
||||
(baseline) and the running counter are set to the same value. Call this
|
||||
before encrypting if you need a deterministic starting point.
|
||||
|
||||
```c
|
||||
void secCipherDestroy(SecCipherT *c);
|
||||
```
|
||||
|
||||
Securely zeroes and frees the cipher context.
|
||||
Securely zeroes the cipher context (key and counter state) and frees the
|
||||
memory.
|
||||
|
||||
## Example
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Full Key Exchange
|
||||
|
||||
|
|
@ -187,12 +371,12 @@ int pubLen = SEC_DH_KEY_SIZE;
|
|||
secDhGetPublicKey(dh, myPub, &pubLen);
|
||||
// ... send myPub to remote, receive remotePub ...
|
||||
|
||||
// Compute shared secret and derive a 16-byte key
|
||||
// Compute shared secret and derive a 16-byte XTEA key
|
||||
secDhComputeSecret(dh, remotePub, SEC_DH_KEY_SIZE);
|
||||
|
||||
uint8_t key[SEC_XTEA_KEY_SIZE];
|
||||
secDhDeriveKey(dh, key, SEC_XTEA_KEY_SIZE);
|
||||
secDhDestroy(dh);
|
||||
secDhDestroy(dh); // private key no longer needed
|
||||
|
||||
// Create cipher and encrypt
|
||||
SecCipherT *cipher = secCipherCreate(key);
|
||||
|
|
@ -200,8 +384,7 @@ uint8_t message[] = "Secret message";
|
|||
secCipherCrypt(cipher, message, sizeof(message));
|
||||
// message is now encrypted
|
||||
|
||||
// Decrypt (same operation -- CTR mode is symmetric)
|
||||
// Reset counter first if using the same cipher context
|
||||
// Decrypt (reset counter first, then apply same operation)
|
||||
secCipherSetNonce(cipher, 0, 0);
|
||||
secCipherCrypt(cipher, message, sizeof(message));
|
||||
// message is now plaintext again
|
||||
|
|
@ -209,10 +392,10 @@ secCipherCrypt(cipher, message, sizeof(message));
|
|||
secCipherDestroy(cipher);
|
||||
```
|
||||
|
||||
### Standalone Encryption
|
||||
### Standalone Encryption (Without DH)
|
||||
|
||||
```c
|
||||
// XTEA-CTR can be used independently of DH
|
||||
// XTEA-CTR can be used independently of Diffie-Hellman
|
||||
uint8_t key[SEC_XTEA_KEY_SIZE] = { /* your key */ };
|
||||
SecCipherT *c = secCipherCreate(key);
|
||||
|
||||
|
|
@ -223,31 +406,24 @@ secCipherCrypt(c, data, sizeof(data)); // encrypt in place
|
|||
secCipherDestroy(c);
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
### Random Number Generation
|
||||
|
||||
### BigNum Arithmetic
|
||||
```c
|
||||
// Seed from hardware
|
||||
uint8_t hwEntropy[16];
|
||||
secRngGatherEntropy(hwEntropy, sizeof(hwEntropy));
|
||||
secRngSeed(hwEntropy, sizeof(hwEntropy));
|
||||
|
||||
All modular arithmetic uses a 1024-bit big number type (`BigNumT`)
|
||||
stored as 32 x `uint32_t` words in little-endian order. Operations:
|
||||
// Stir in user-derived entropy (keyboard timing, etc.)
|
||||
uint8_t userEntropy[4];
|
||||
// ... gather from timing events ...
|
||||
secRngAddEntropy(userEntropy, sizeof(userEntropy));
|
||||
|
||||
- Add, subtract, compare, shift-left-1, bit test
|
||||
- Montgomery multiplication (CIOS with implicit right-shift)
|
||||
- Modular exponentiation (left-to-right binary square-and-multiply)
|
||||
// Generate random bytes
|
||||
uint8_t randomBuf[32];
|
||||
secRngBytes(randomBuf, sizeof(randomBuf));
|
||||
```
|
||||
|
||||
### Montgomery Multiplication
|
||||
|
||||
The CIOS (Coarsely Integrated Operand Scanning) variant computes
|
||||
`a * b * R^-1 mod m` in a single pass with implicit division by the
|
||||
word base. Constants are computed once on first DH use:
|
||||
|
||||
- `R^2 mod p` -- via 2048 iterations of shift-and-conditional-subtract
|
||||
- `-p[0]^-1 mod 2^32` -- via Newton's method (5 iterations)
|
||||
|
||||
### Secure Zeroing
|
||||
|
||||
Key material is erased using a volatile-pointer loop that the compiler
|
||||
cannot optimize away, preventing sensitive data from lingering in
|
||||
memory.
|
||||
|
||||
## Building
|
||||
|
||||
|
|
@ -256,4 +432,23 @@ make # builds ../lib/libsecurity.a
|
|||
make clean # removes objects and library
|
||||
```
|
||||
|
||||
Target: DJGPP cross-compiler, 486+ CPU.
|
||||
Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler
|
||||
flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`.
|
||||
|
||||
Objects are placed in `../obj/security/`, the library in `../lib/`.
|
||||
|
||||
No external dependencies -- the library is self-contained. It uses only
|
||||
DJGPP's `<pc.h>`, `<sys/farptr.h>`, and `<go32.h>` for hardware entropy
|
||||
collection (PIT and BIOS tick count access).
|
||||
|
||||
|
||||
## Files
|
||||
|
||||
- `security.h` -- Public API header (types, constants, function prototypes)
|
||||
- `security.c` -- Complete implementation (bignum, Montgomery, DH, XTEA, RNG)
|
||||
- `Makefile` -- DJGPP cross-compilation build rules
|
||||
|
||||
|
||||
## Used By
|
||||
|
||||
- `seclink/` -- Secure serial link (DH handshake, cipher creation, RNG seeding)
|
||||
|
|
|
|||
263
tasks/README.md
263
tasks/README.md
|
|
@ -1,46 +1,58 @@
|
|||
# taskswitch -- Cooperative Task Switching Library for DJGPP
|
||||
# taskswitch -- Cooperative Task Switching Library
|
||||
|
||||
A lightweight cooperative multitasking library targeting DJGPP (i386 protected
|
||||
mode DOS). Tasks voluntarily yield the CPU with `tsYield()`. A credit-based
|
||||
weighted round-robin scheduler ensures every task runs while giving
|
||||
higher-priority tasks proportionally more CPU time.
|
||||
Cooperative (non-preemptive) multitasking library for DJGPP/DPMI (DOS
|
||||
protected mode). Part of the DVX GUI project.
|
||||
|
||||
Tasks voluntarily yield the CPU by calling `tsYield()`. A credit-based
|
||||
weighted round-robin scheduler gives higher-priority tasks proportionally
|
||||
more CPU time while guaranteeing that low-priority tasks are never
|
||||
starved. A priority-10 task gets 11 turns per scheduling round; a
|
||||
priority-0 task gets 1 -- but it always runs eventually.
|
||||
|
||||
The task array is backed by stb_ds and grows dynamically. Terminated
|
||||
task slots are recycled, so there is no fixed upper limit on the number
|
||||
of tasks created over the lifetime of the application.
|
||||
|
||||
|
||||
## Why Cooperative?
|
||||
|
||||
DOS is single-threaded. DPMI provides no timer-based preemption. The
|
||||
DVX GUI event model is inherently single-threaded: one compositor, one
|
||||
input queue, one window stack. Preemptive switching would require
|
||||
locking around every GUI call for no benefit. Cooperative switching
|
||||
lets each task yield at safe points, avoiding synchronization entirely.
|
||||
|
||||
The task array grows dynamically using stb_ds and terminated task slots are
|
||||
recycled, so there is no fixed upper limit on the number of tasks created
|
||||
over the lifetime of the application.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Description |
|
||||
|-----------------------|------------------------------------------|
|
||||
| `taskswitch.h` | Public API -- types, constants, functions |
|
||||
| `taskswitch.c` | Implementation |
|
||||
| `demo.c` | Example program exercising every feature |
|
||||
| `thirdparty/stb_ds.h` | Dynamic array/hashmap library (stb) |
|
||||
|-----------------------|----------------------------------------------------|
|
||||
| `taskswitch.h` | Public API -- types, constants, function prototypes |
|
||||
| `taskswitch.c` | Implementation (scheduler, context switch, slots) |
|
||||
| `demo.c` | Standalone test harness exercising all features |
|
||||
| `thirdparty/stb_ds.h` | stb dynamic array/hashmap library (third-party) |
|
||||
| `Makefile` | DJGPP cross-compilation build rules |
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
Cross-compiling from Linux:
|
||||
Cross-compile from Linux:
|
||||
|
||||
```
|
||||
make
|
||||
```
|
||||
|
||||
Clean:
|
||||
|
||||
```
|
||||
make clean
|
||||
make # builds ../lib/libtasks.a
|
||||
make demo # also builds ../bin/tsdemo.exe
|
||||
make clean # removes objects, library, and demo binary
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
| Path | Description |
|
||||
|-------------------|----------------------------|
|
||||
|---------------------|----------------------|
|
||||
| `../lib/libtasks.a` | Static library |
|
||||
| `../obj/tasks/` | Object files |
|
||||
| `../bin/tsdemo.exe` | Demo executable |
|
||||
|
||||
|
||||
## Quick Start
|
||||
|
||||
```c
|
||||
|
|
@ -69,86 +81,91 @@ int main(void) {
|
|||
}
|
||||
```
|
||||
|
||||
|
||||
## Lifecycle
|
||||
|
||||
1. **`tsInit()`** -- Initialize the task system. The calling context
|
||||
(typically `main`) becomes task 0 with `TS_PRIORITY_NORMAL`.
|
||||
1. `tsInit()` -- Initialize the task system. The calling context
|
||||
(typically `main`) becomes task 0 with `TS_PRIORITY_NORMAL`. No
|
||||
separate stack is allocated for task 0 -- it uses the process stack.
|
||||
|
||||
2. **`tsCreate(...)`** -- Create tasks. Each gets a name, entry function,
|
||||
argument pointer, stack size (0 for the 8 KB default), and a priority.
|
||||
Returns the task ID (>= 0) or a negative error code. Terminated task
|
||||
slots are reused automatically.
|
||||
2. `tsCreate(...)` -- Create tasks. Each gets a name, entry function,
|
||||
argument pointer, stack size (0 for the default), and a priority.
|
||||
Returns the task ID (>= 0) or a negative error code. Terminated
|
||||
task slots are reused automatically.
|
||||
|
||||
3. **`tsYield()`** -- Call from any task (including main) to hand the CPU to
|
||||
the next eligible task.
|
||||
3. `tsYield()` -- Call from any task (including main) to hand the CPU
|
||||
to the next eligible task. This is the sole mechanism for task
|
||||
switching.
|
||||
|
||||
4. **`tsShutdown()`** -- Free all task stacks and the task array.
|
||||
4. `tsShutdown()` -- Free all task stacks and the task array.
|
||||
|
||||
Tasks terminate by returning from their entry function or by calling
|
||||
`tsExit()`. The main task (id 0) must never call `tsExit()`. When a task
|
||||
terminates, its stack is freed immediately and its slot becomes available
|
||||
for reuse by the next `tsCreate()` call.
|
||||
`tsExit()`. The main task (id 0) must never call `tsExit()`. When a
|
||||
task terminates, its stack is freed immediately and its slot becomes
|
||||
available for reuse by the next `tsCreate()` call.
|
||||
|
||||
|
||||
## API Reference
|
||||
|
||||
### Initialisation and Teardown
|
||||
### Initialization and Teardown
|
||||
|
||||
| Function | Signature | Description |
|
||||
|----------------|------------------------------|--------------------------------------------------------------------------------------|
|
||||
| `tsInit` | `int32_t tsInit(void)` | Initialise the library. Returns `TS_OK` or a negative error code. |
|
||||
|--------------|-------------------------|--------------------------------------------------------------------|
|
||||
| `tsInit` | `int32_t tsInit(void)` | Initialize the library. Returns `TS_OK` or a negative error code. |
|
||||
| `tsShutdown` | `void tsShutdown(void)` | Free all resources. Safe to call even if `tsInit` was never called. |
|
||||
|
||||
### Task Creation and Termination
|
||||
|
||||
| Function | Signature | Description |
|
||||
|------------|----------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
|
||||
| `tsCreate` | `int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t ss, int32_t pri)` | Create a ready task. Returns the task ID (>= 0) or a negative error code. Pass 0 for `ss` to use `TS_DEFAULT_STACK_SIZE` (8 KB). Reuses terminated task slots when available. |
|
||||
| `tsExit` | `void tsExit(void)` | Terminate the calling task. Must not be called from the main task. |
|
||||
| `tsKill` | `int32_t tsKill(uint32_t taskId)` | Forcibly terminate another task. Frees its stack and marks the slot for reuse. Cannot kill the main task (id 0) or the calling task (use `tsExit` instead). |
|
||||
|------------|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
|
||||
| `tsCreate` | `int32_t tsCreate(const char *name, TaskEntryT entry, void *arg, uint32_t ss, int32_t pri)` | Create a ready task. Returns the task ID (>= 0) or a negative error code. Pass 0 for `ss` to use `TS_DEFAULT_STACK_SIZE`. Reuses terminated slots. |
|
||||
| `tsExit` | `void tsExit(void)` | Terminate the calling task. Must not be called from the main task. Never returns. |
|
||||
| `tsKill` | `int32_t tsKill(uint32_t taskId)` | Forcibly terminate another task. Cannot kill main (id 0) or self (use `tsExit` instead). |
|
||||
|
||||
### Scheduling
|
||||
|
||||
| Function | Signature | Description |
|
||||
|-----------|-----------------------|--------------------------------------------------------------------------|
|
||||
|-----------|----------------------|-----------------------------------------------------------------|
|
||||
| `tsYield` | `void tsYield(void)` | Voluntarily relinquish the CPU to the next eligible ready task. |
|
||||
|
||||
### Pausing and Resuming
|
||||
|
||||
| Function | Signature | Description |
|
||||
|------------|-----------------------------------|--------------------------------------------------------------------------------------------------------------------|
|
||||
| `tsPause` | `int32_t tsPause(uint32_t id)` | Pause a task. The main task (id 0) cannot be paused. If a task pauses itself, an implicit yield occurs. |
|
||||
| `tsResume` | `int32_t tsResume(uint32_t id)` | Resume a paused task. Its credits are refilled to `priority + 1` so it is not penalised for having been paused. |
|
||||
|------------|---------------------------------|------------------------------------------------------------------------------------------------------------|
|
||||
| `tsPause` | `int32_t tsPause(uint32_t id)` | Pause a task. Main task (id 0) cannot be paused. Self-pause triggers an implicit yield. |
|
||||
| `tsResume` | `int32_t tsResume(uint32_t id)` | Resume a paused task. Credits are refilled so it is not penalized for having been paused. |
|
||||
|
||||
### Priority
|
||||
|
||||
| Function | Signature | Description |
|
||||
|-----------------|--------------------------------------------------|---------------------------------------------------------------------------------------------------|
|
||||
| `tsSetPriority` | `int32_t tsSetPriority(uint32_t id, int32_t pri)`| Change a task's priority. Credits are reset to `pri + 1` so the change takes effect immediately. |
|
||||
|-----------------|---------------------------------------------------|-----------------------------------------------------------------------------------|
|
||||
| `tsSetPriority` | `int32_t tsSetPriority(uint32_t id, int32_t pri)` | Change a task's priority. Credits are reset so the change takes effect immediately. |
|
||||
| `tsGetPriority` | `int32_t tsGetPriority(uint32_t id)` | Return the task's priority, or `TS_ERR_PARAM` on an invalid ID. |
|
||||
|
||||
### Crash Recovery
|
||||
|
||||
| Function | Signature | Description |
|
||||
|-------------------|---------------------------------|-----------------------------------------------------------------------------------------------------------------|
|
||||
| `tsRecoverToMain` | `void tsRecoverToMain(void)` | Reset scheduler state to the main task (id 0) after a `longjmp` from a signal handler. Call before `tsKill` on the crashed task. This fixes the scheduler's bookkeeping when a non-main task crashes and execution is transferred back to main via `longjmp`. |
|
||||
|-------------------|------------------------------|------------------------------------------------------------------------------------------------------------------|
|
||||
| `tsRecoverToMain` | `void tsRecoverToMain(void)` | Reset scheduler state to task 0 after a `longjmp` from a signal handler. Call before `tsKill` on the crashed task. The crashed task's slot is NOT freed -- call `tsKill` afterward. |
|
||||
|
||||
### Query
|
||||
|
||||
| Function | Signature | Description |
|
||||
|-----------------|-----------------------------------------|---------------------------------------------------------------|
|
||||
|-----------------|--------------------------------------|--------------------------------------------------------|
|
||||
| `tsGetState` | `TaskStateE tsGetState(uint32_t id)` | Return the task's state enum value. |
|
||||
| `tsCurrentId` | `uint32_t tsCurrentId(void)` | Return the ID of the currently running task. |
|
||||
| `tsGetName` | `const char *tsGetName(uint32_t id)` | Return the task's name string, or `NULL` on invalid ID. |
|
||||
| `tsActiveCount` | `uint32_t tsActiveCount(void)` | Return the number of non-terminated tasks. |
|
||||
|
||||
|
||||
## Constants
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Name | Value | Meaning |
|
||||
|----------------|-------|--------------------------------------|
|
||||
|----------------|-------|--------------------------------------------------|
|
||||
| `TS_OK` | 0 | Success |
|
||||
| `TS_ERR_INIT` | -1 | Library not initialised |
|
||||
| `TS_ERR_INIT` | -1 | Library not initialized |
|
||||
| `TS_ERR_PARAM` | -2 | Invalid parameter |
|
||||
| `TS_ERR_FULL` | -3 | Task table full (unused, kept for compatibility) |
|
||||
| `TS_ERR_NOMEM` | -4 | Memory allocation failed |
|
||||
|
|
@ -163,18 +180,21 @@ for reuse by the next `tsCreate()` call.
|
|||
| `TS_PRIORITY_HIGH` | 10 | 11 |
|
||||
|
||||
Any non-negative `int32_t` may be used as a priority. The presets are
|
||||
provided for convenience.
|
||||
provided for convenience. In the DVX Shell, the main task runs at
|
||||
`TS_PRIORITY_HIGH` to keep the UI responsive; app tasks default to
|
||||
`TS_PRIORITY_NORMAL`.
|
||||
|
||||
### Defaults
|
||||
|
||||
| Name | Value | Description |
|
||||
|-------------------------|-------|-------------------------|
|
||||
| `TS_DEFAULT_STACK_SIZE` | 8192 | Default stack per task |
|
||||
|-------------------------|-------|------------------------|
|
||||
| `TS_DEFAULT_STACK_SIZE` | 32768 | Default stack per task |
|
||||
| `TS_NAME_MAX` | 32 | Max task name length |
|
||||
|
||||
|
||||
## Types
|
||||
|
||||
### `TaskStateE`
|
||||
### TaskStateE
|
||||
|
||||
```c
|
||||
typedef enum {
|
||||
|
|
@ -185,64 +205,83 @@ typedef enum {
|
|||
} TaskStateE;
|
||||
```
|
||||
|
||||
### `TaskEntryT`
|
||||
Only Ready tasks participate in scheduling. Running is cosmetic (marks
|
||||
the currently executing task). Paused tasks are skipped until explicitly
|
||||
resumed. Terminated slots are recycled by `tsCreate`.
|
||||
|
||||
### TaskEntryT
|
||||
|
||||
```c
|
||||
typedef void (*TaskEntryT)(void *arg);
|
||||
```
|
||||
|
||||
The signature every task entry function must follow. `arg` is the pointer
|
||||
passed to `tsCreate`.
|
||||
The signature every task entry function must follow. The `arg` parameter
|
||||
is the pointer passed to `tsCreate`.
|
||||
|
||||
|
||||
## Scheduler Details
|
||||
|
||||
The scheduler is a **credit-based weighted round-robin**.
|
||||
The scheduler is a credit-based weighted round-robin, a variant of the
|
||||
Linux 2.4 goodness() scheduler.
|
||||
|
||||
1. Every ready task holds a credit counter initialised to `priority + 1`.
|
||||
2. When `tsYield()` is called, the scheduler scans tasks starting one past
|
||||
the current task (wrapping around) looking for a ready task with
|
||||
1. Every ready task holds a credit counter initialized to `priority + 1`.
|
||||
2. When `tsYield()` is called, the scheduler scans tasks starting one
|
||||
past the current task (wrapping around) looking for a ready task with
|
||||
credits > 0. When found, that task's credits are decremented and it
|
||||
becomes the running task.
|
||||
3. When **no** ready task has credits remaining, every ready task is
|
||||
refilled to `priority + 1` and the scan repeats.
|
||||
3. When no ready task has credits remaining, every ready task is
|
||||
refilled to `priority + 1` (one "epoch") and the scan repeats.
|
||||
|
||||
This means a priority-10 task receives 11 turns for every 1 turn a
|
||||
priority-0 task receives, but the low-priority task still runs -- it is
|
||||
never starved.
|
||||
priority-0 task receives, but the low-priority task still runs -- it
|
||||
is never starved.
|
||||
|
||||
Credits are also refilled when:
|
||||
|
||||
- A task is **created** (`tsCreate`) -- starts with `priority + 1`.
|
||||
- A task is **resumed** (`tsResume`) -- refilled so it is not penalised.
|
||||
- A task's **priority changes** (`tsSetPriority`) -- reset to `new + 1`.
|
||||
- A task is created (`tsCreate`) -- starts with `priority + 1`.
|
||||
- A task is resumed (`tsResume`) -- refilled so it runs promptly.
|
||||
- A task's priority changes (`tsSetPriority`) -- reset to `new + 1`.
|
||||
|
||||
|
||||
## Task Slot Management
|
||||
|
||||
The task array is a stb_ds dynamic array that grows automatically as needed.
|
||||
The task array is a stb_ds dynamic array that grows automatically.
|
||||
Each slot has an `allocated` flag:
|
||||
|
||||
- **`tsCreate()`** scans for the first unallocated slot (starting at index 1,
|
||||
since slot 0 is always the main task). If no free slot exists, the array
|
||||
is extended with `arrput()`.
|
||||
- **`tsExit()`** frees the terminated task's stack immediately and marks the
|
||||
slot as unallocated, making it available for the next `tsCreate()` call.
|
||||
- Task IDs are stable array indices. Slots are never removed or reordered,
|
||||
so a task ID remains valid for queries until the slot is recycled.
|
||||
- `tsCreate()` scans for the first unallocated slot (starting at index
|
||||
1, since slot 0 is always the main task). If no free slot exists, the
|
||||
array is extended with `arrput()`.
|
||||
- `tsExit()` and `tsKill()` free the terminated task's stack immediately
|
||||
and mark the slot as unallocated, making it available for the next
|
||||
`tsCreate()` call.
|
||||
- Task IDs are stable array indices. Slots are never removed or
|
||||
reordered, so a task ID remains valid for queries until the slot is
|
||||
recycled.
|
||||
|
||||
This supports long-running applications (like the DVX Shell) that
|
||||
create and destroy many tasks over their lifetime without unbounded
|
||||
memory growth.
|
||||
|
||||
This design supports long-running applications that create and destroy
|
||||
many tasks over their lifetime without unbounded memory growth.
|
||||
|
||||
## Context Switch Internals
|
||||
|
||||
Context switching is performed entirely in inline assembly with both i386
|
||||
and x86_64 code paths.
|
||||
Context switching uses inline assembly with both i386 and x86_64 code
|
||||
paths. The `contextSwitch` function is marked `noinline` to preserve
|
||||
callee-saved register assumptions.
|
||||
|
||||
Why inline asm instead of setjmp/longjmp: setjmp/longjmp only save
|
||||
callee-saved registers and do not give control over the stack pointer
|
||||
in a portable way. New tasks need a fresh stack with the instruction
|
||||
pointer set to a trampoline -- setjmp cannot bootstrap that. The asm
|
||||
approach also avoids ABI differences in jmp_buf layout across DJGPP
|
||||
versions.
|
||||
|
||||
### i386 (DJGPP target)
|
||||
|
||||
Six callee-saved values are saved and restored per switch:
|
||||
|
||||
| Register | Offset | Purpose |
|
||||
|----------|--------|-----------------------------------------|
|
||||
|----------|--------|------------------------------------------|
|
||||
| EBX | 0 | Callee-saved general purpose |
|
||||
| ESI | 4 | Callee-saved general purpose |
|
||||
| EDI | 8 | Callee-saved general purpose |
|
||||
|
|
@ -250,12 +289,12 @@ Six callee-saved values are saved and restored per switch:
|
|||
| ESP | 16 | Stack pointer |
|
||||
| EIP | 20 | Resume address (captured as local label) |
|
||||
|
||||
### x86_64 (for native testing)
|
||||
### x86_64 (for native Linux testing)
|
||||
|
||||
Eight callee-saved values are saved and restored per switch:
|
||||
|
||||
| Register | Offset | Purpose |
|
||||
|----------|--------|-----------------------------------------|
|
||||
|----------|--------|------------------------------------------|
|
||||
| RBX | 0 | Callee-saved general purpose |
|
||||
| R12 | 8 | Callee-saved general purpose |
|
||||
| R13 | 16 | Callee-saved general purpose |
|
||||
|
|
@ -265,42 +304,54 @@ Eight callee-saved values are saved and restored per switch:
|
|||
| RSP | 48 | Stack pointer |
|
||||
| RIP | 56 | Resume address (RIP-relative lea) |
|
||||
|
||||
The save and restore pointers are passed into the assembly block via GCC
|
||||
register constraints. Segment registers are not saved because DJGPP runs
|
||||
in a flat protected-mode environment where CS, DS, ES, and SS share the
|
||||
same base.
|
||||
Segment registers are not saved because DJGPP runs in a flat
|
||||
protected-mode environment where CS, DS, ES, and SS share the same
|
||||
base.
|
||||
|
||||
New tasks have their initial stack pointer set to a 16-byte-aligned
|
||||
region at the top of a malloc'd stack, with the instruction pointer
|
||||
set to an internal trampoline that calls the user's entry function
|
||||
and then `tsExit()`.
|
||||
|
||||
New tasks have their initial stack pointer set to a 16-byte-aligned region
|
||||
at the top of a `malloc`'d stack, with the instruction pointer set to an
|
||||
internal trampoline that calls the user's entry function and then `tsExit()`.
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Cooperative only** -- tasks must call `tsYield()` (or `tsPause`/`tsExit`)
|
||||
to allow other tasks to run. A task that never yields blocks everything.
|
||||
- **Not interrupt-safe** -- the library uses no locking or `volatile` module
|
||||
state. Do not call library functions from interrupt handlers.
|
||||
- **Cooperative only** -- tasks must call `tsYield()` (or
|
||||
`tsPause`/`tsExit`) to allow other tasks to run. A task that never
|
||||
yields blocks everything.
|
||||
- **Not interrupt-safe** -- no locking or volatile module state. Do not
|
||||
call library functions from interrupt handlers.
|
||||
- **Single-threaded** -- designed for one CPU under DOS protected mode.
|
||||
- **Stack overflow is not detected** -- size the stack appropriately for each
|
||||
task's needs.
|
||||
- **Stack overflow is not detected** -- size the stack appropriately for
|
||||
each task's needs.
|
||||
|
||||
|
||||
## Demo
|
||||
|
||||
`demo.c` exercises five phases:
|
||||
|
||||
1. **Priority scheduling** -- creates tasks at low, normal, and high priority.
|
||||
All tasks run, but the high-priority task gets significantly more turns.
|
||||
2. **Pause** -- pauses one task mid-run and shows it stops being scheduled.
|
||||
1. **Priority scheduling** -- creates tasks at low, normal, and high
|
||||
priority. All tasks run, but the high-priority task gets significantly
|
||||
more turns.
|
||||
2. **Pause** -- pauses one task mid-run and shows it stops being
|
||||
scheduled.
|
||||
3. **Resume** -- resumes the paused task and shows it picks up where it
|
||||
left off.
|
||||
4. **Priority boost** -- raises the low-priority task above all others and
|
||||
shows it immediately gets more turns.
|
||||
5. **Slot reuse** -- creates three waves of short-lived tasks that terminate
|
||||
and shows subsequent waves reuse the same task IDs.
|
||||
4. **Priority boost** -- raises the low-priority task above all others
|
||||
and shows it immediately gets more turns.
|
||||
5. **Slot reuse** -- creates three waves of short-lived tasks that
|
||||
terminate and shows subsequent waves reuse the same task IDs.
|
||||
|
||||
Build and run:
|
||||
|
||||
```
|
||||
make
|
||||
make demo
|
||||
tsdemo
|
||||
```
|
||||
|
||||
|
||||
## Third-Party Dependencies
|
||||
|
||||
- **stb_ds.h** (Sean Barrett) -- dynamic array and hashmap library.
|
||||
Located in `thirdparty/stb_ds.h`. Used for the task control block
|
||||
array. Public domain / MIT licensed.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,17 @@
|
|||
# SecLink Terminal Demo
|
||||
|
||||
DOS terminal emulator combining the DVX windowed GUI with SecLink
|
||||
encrypted serial communication. Connects to a remote BBS through the
|
||||
SecLink proxy, providing a full ANSI terminal in a DVX-style
|
||||
window with encrypted transport.
|
||||
Standalone DOS terminal emulator combining the DVX windowed GUI with
|
||||
SecLink encrypted serial communication. Part of the DVX GUI project.
|
||||
|
||||
This is NOT a DXE app -- it is a freestanding program with its own
|
||||
`main()` that initializes the DVX GUI directly and manages its own
|
||||
event loop. Unlike the DXE apps (progman, notepad, clock, dvxdemo)
|
||||
which run inside the DVX Shell, this program demonstrates how to use
|
||||
the DVX widget system outside the shell framework.
|
||||
|
||||
Connects to a remote BBS through the SecLink proxy, providing a full
|
||||
ANSI terminal in a DVX-style window with encrypted transport.
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||
|
|
@ -14,7 +22,7 @@ termdemo (DOS, 86Box)
|
|||
|
|
||||
+--- SecLink encrypted serial link
|
||||
| |
|
||||
| +--- packet HDLC framing, CRC, retransmit
|
||||
| +--- packet HDLC framing, CRC-16, Go-Back-N ARQ
|
||||
| +--- security DH key exchange, XTEA-CTR cipher
|
||||
| +--- rs232 ISR-driven UART I/O
|
||||
|
|
||||
|
|
@ -33,6 +41,7 @@ All traffic between the terminal and the proxy is encrypted via
|
|||
XTEA-CTR on SecLink channel 0. The proxy decrypts and forwards
|
||||
plaintext to the BBS over telnet.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
|
|
@ -40,10 +49,12 @@ termdemo [com_port] [baud_rate]
|
|||
```
|
||||
|
||||
| Argument | Default | Description |
|
||||
|-------------|---------|------------------------------|
|
||||
|-------------|---------|--------------------------|
|
||||
| `com_port` | 1 | COM port number (1-4) |
|
||||
| `baud_rate` | 115200 | Serial baud rate |
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
termdemo # COM1 at 115200
|
||||
termdemo 2 # COM2 at 115200
|
||||
|
|
@ -51,25 +62,33 @@ termdemo 1 57600 # COM1 at 57600
|
|||
termdemo -h # show usage
|
||||
```
|
||||
|
||||
|
||||
## Startup Sequence
|
||||
|
||||
1. Seed the RNG from hardware entropy
|
||||
2. Open SecLink on the specified COM port (8N1, no handshake)
|
||||
3. Perform DH key exchange (blocks until the proxy completes its side)
|
||||
4. Initialize the DVX GUI (1024x768, 16bpp VESA)
|
||||
5. Create a resizable terminal window with menu bar and status bar
|
||||
6. Enter the main loop
|
||||
1. Seed the RNG from hardware entropy (PIT-based on DOS).
|
||||
2. Open SecLink on the specified COM port (8N1, no handshake).
|
||||
3. Perform DH key exchange (blocks until the proxy completes its side).
|
||||
4. Initialize the DVX GUI (1024x768, 16bpp VESA).
|
||||
5. Create a resizable terminal window with menu bar and status bar.
|
||||
6. Register an idle callback so serial data is polled during GUI idle.
|
||||
7. Enter the main loop.
|
||||
|
||||
The handshake completes in text mode before the GUI starts, so the
|
||||
DOS console shows progress messages during connection setup.
|
||||
|
||||
|
||||
## Main Loop
|
||||
|
||||
Each iteration:
|
||||
|
||||
1. `dvxUpdate()` -- process mouse, keyboard, paint, and window events
|
||||
2. `secLinkPoll()` -- read serial data, decrypt, deliver to ring buffer
|
||||
3. `wgtAnsiTermPoll()` -- drain ring buffer into the ANSI parser
|
||||
1. `secLinkPoll()` -- read serial data, decrypt, deliver to ring buffer.
|
||||
2. `dvxUpdate()` -- process mouse, keyboard, paint, and window events.
|
||||
During paint, the terminal widget calls `commRead` to drain the ring
|
||||
buffer and render new data.
|
||||
|
||||
An idle callback also calls `secLinkPoll()` so incoming data is
|
||||
processed even when the user is not interacting with the terminal.
|
||||
|
||||
|
||||
## Data Flow
|
||||
|
||||
|
|
@ -81,9 +100,13 @@ Keyboard -> widgetAnsiTermOnKey() -> commWrite()
|
|||
-> secLinkSend() -> serial -> proxy -> BBS
|
||||
```
|
||||
|
||||
A 4KB ring buffer bridges the SecLink receive callback (which fires
|
||||
during `secLinkPoll()`) and the terminal widget's comm read interface
|
||||
(which is polled by `wgtAnsiTermPoll()`).
|
||||
A 4KB ring buffer (`RECV_BUF_SIZE`) bridges the SecLink receive
|
||||
callback (which fires asynchronously during `secLinkPoll()`) and the
|
||||
terminal widget's comm read interface (which is polled synchronously
|
||||
during the widget paint cycle). This decoupling is necessary because
|
||||
the callback can fire at any time during polling, but the terminal
|
||||
widget expects to read data synchronously.
|
||||
|
||||
|
||||
## GUI
|
||||
|
||||
|
|
@ -97,6 +120,7 @@ cursor control, SGR colors (16-color CGA palette), erase, scroll,
|
|||
insert/delete lines, and DEC private modes (cursor visibility, line
|
||||
wrap).
|
||||
|
||||
|
||||
## Test Setup
|
||||
|
||||
1. Start the SecLink proxy on the Linux host:
|
||||
|
|
@ -106,7 +130,7 @@ wrap).
|
|||
```
|
||||
|
||||
2. Configure 86Box with a COM port pointing at the proxy's listen port
|
||||
(TCP client mode, port 2323, no telnet negotiation)
|
||||
(TCP client mode, port 2323, no telnet negotiation).
|
||||
|
||||
3. Run the terminal inside 86Box:
|
||||
|
||||
|
|
@ -115,21 +139,26 @@ wrap).
|
|||
```
|
||||
|
||||
4. The handshake completes, the GUI appears, and BBS output is
|
||||
displayed in the terminal window
|
||||
displayed in the terminal window.
|
||||
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
make # builds ../bin/termdemo.exe
|
||||
make # builds ../bin/termdemo.exe (and all dependency libs)
|
||||
make clean # removes objects and binary
|
||||
```
|
||||
|
||||
The Makefile builds all dependency libraries automatically. Objects
|
||||
are placed in `../obj/termdemo/`, the binary in `../bin/`.
|
||||
The Makefile automatically builds all dependency libraries before
|
||||
linking. Objects are placed in `../obj/termdemo/`, the binary in
|
||||
`../bin/`.
|
||||
|
||||
Target: DJGPP cross-compiler, 486+ CPU, VESA VBE 2.0+ video.
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
All libraries are in `../lib/`:
|
||||
All libraries are built into `../lib/`:
|
||||
|
||||
| Library | Purpose |
|
||||
|------------------|--------------------------------------|
|
||||
|
|
@ -139,7 +168,6 @@ All libraries are in `../lib/`:
|
|||
| `libsecurity.a` | DH key exchange and XTEA cipher |
|
||||
| `librs232.a` | ISR-driven UART serial driver |
|
||||
|
||||
Target: DJGPP cross-compiler, 486+ CPU, VESA VBE 2.0+ video.
|
||||
|
||||
## Files
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue