Files
lorakiss/test/main_simulator.cpp
Maze X 20b3aae1e3 Add Python KISS client and integration test framework
- test/kiss_client.py: Full-featured KISS modem client library
  Supports all 14 config commands, KISS frame encoding/decoding,
  signal quality reporting, packet send/receive
  Can communicate over serial ports or file-like objects (stdin/stdout)

- test/test_integration.py: Integration test suite using kiss_client.py
  Tests radio configuration, parameter setting, KISS frame encoding/decoding
  Designed to run against modem or simulator
  Run with: python3 test/test_integration.py

- test/main_simulator.cpp: Hardware simulator scaffold for native platform
  Mocks SX126X radio with configurable state
  Communicates via stdin/stdout for easy testing
  Note: Full compilation requires Arduino.h mock (future work)

Verified: seeed_xiao_s3_wio_sx1262 and heltec_v3 still build without errors
2026-03-27 17:46:33 +01:00

205 lines
7.6 KiB
C++

/*
* Dummy LoRa Radio Hardware Simulator
*
* Simulates a LoRa radio on the native (host) platform using stdio for communication.
* Allows testing the KISS protocol and modem logic without hardware.
*
* Compiled as PlatformIO native environment for integration testing.
*/
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
/* Include the KISS implementation directly */
#include "../src/kiss.cpp"
/* ─────────────────────────────────────────────────────────────────────────── */
/* Simulated Radio State */
/* ─────────────────────────────────────────────────────────────────────────── */
struct {
uint32_t freq_khz;
uint32_t bw_hz;
uint8_t sf;
uint8_t cr;
int8_t power_dbm;
uint8_t syncword;
bool rx_active;
uint8_t rx_buf[256];
int rx_len;
int8_t rx_snr;
int16_t rx_rssi;
} radio_state = {
.freq_khz = 868000,
.bw_hz = 125000,
.sf = 7,
.cr = 5,
.power_dbm = 14,
.syncword = 0x12,
.rx_active = true,
.rx_len = 0,
};
/* ─────────────────────────────────────────────────────────────────────────── */
/* Mock C API (must match radio.h) */
/* ─────────────────────────────────────────────────────────────────────────── */
typedef struct {
uint32_t freq_khz;
uint32_t bw_hz;
uint8_t sf;
uint8_t cr;
int8_t power_dbm;
uint8_t syncword;
} radio_config_t;
typedef struct {
int8_t snr;
int16_t rssi;
} radio_rx_info_t;
int radio_init(void) {
fprintf(stderr, "[radio] Initialized: %lu kHz, %lu Hz BW, SF %d, CR %d, %d dBm\n",
radio_state.freq_khz, radio_state.bw_hz, radio_state.sf, radio_state.cr,
radio_state.power_dbm);
return 0;
}
int radio_tx(const uint8_t *data, size_t len) {
fprintf(stderr, "[radio] TX %zu bytes at %lu kHz\n", len, radio_state.freq_khz);
return 0;
}
int radio_rx_start(void) {
radio_state.rx_active = true;
fprintf(stderr, "[radio] RX started\n");
return 0;
}
bool radio_rx_available(void) { return radio_state.rx_len > 0; }
int radio_rx_read(uint8_t *buf, size_t buf_cap, radio_rx_info_t *info) {
if (radio_state.rx_len == 0)
return 0;
size_t n = (radio_state.rx_len < (int)buf_cap) ? radio_state.rx_len : buf_cap;
memcpy(buf, radio_state.rx_buf, n);
if (info) {
info->snr = radio_state.rx_snr;
info->rssi = radio_state.rx_rssi;
}
radio_state.rx_len = 0;
radio_rx_start();
return (int)n;
}
int radio_set_config(const radio_config_t *cfg) {
radio_state.freq_khz = cfg->freq_khz;
radio_state.bw_hz = cfg->bw_hz;
radio_state.sf = cfg->sf;
radio_state.cr = cfg->cr;
radio_state.power_dbm = cfg->power_dbm;
radio_state.syncword = cfg->syncword;
fprintf(stderr, "[radio] Config updated: %lu kHz, %lu Hz BW, SF %d, CR %d, %d dBm\n",
cfg->freq_khz, cfg->bw_hz, cfg->sf, cfg->cr, cfg->power_dbm);
return 0;
}
void radio_get_config(radio_config_t *cfg) {
cfg->freq_khz = radio_state.freq_khz;
cfg->bw_hz = radio_state.bw_hz;
cfg->sf = radio_state.sf;
cfg->cr = radio_state.cr;
cfg->power_dbm = radio_state.power_dbm;
cfg->syncword = radio_state.syncword;
}
/* ─────────────────────────────────────────────────────────────────────────── */
/* Serial I/O Wrapper (stdio) */
/* ─────────────────────────────────────────────────────────────────────────── */
static void set_stdin_nonblocking(void) {
int flags = fcntl(STDIN_FILENO, F_GETFL, 0);
fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK);
}
static int stdin_available(void) {
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
struct timeval tv = {.tv_sec = 0, .tv_usec = 0};
return select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv) > 0;
}
/* ─────────────────────────────────────────────────────────────────────────── */
/* Dummy Serial replacement (for main.cpp) */
/* ─────────────────────────────────────────────────────────────────────────── */
namespace DummySerial {
bool initialized = false;
}
/* Mock Serial object for Arduino compatibility */
class DummySerialClass {
public:
void begin(unsigned long baud) {
fprintf(stderr, "[serial] begin(%lu)\n", baud);
DummySerial::initialized = true;
set_stdin_nonblocking();
}
int available(void) { return stdin_available(); }
int read(void) {
unsigned char c;
if (::read(STDIN_FILENO, &c, 1) == 1)
return c;
return -1;
}
size_t write(const uint8_t *buf, size_t size) {
return fwrite(buf, 1, size, stdout);
}
size_t write(uint8_t c) { return putchar(c) == EOF ? 0 : 1; }
int peek(void) { return -1; }
void flush(void) { fflush(stdout); }
};
DummySerialClass Serial;
/* ─────────────────────────────────────────────────────────────────────────── */
/* Include main modem logic */
/* ─────────────────────────────────────────────────────────────────────────── */
#include "../src/main.cpp"
/* ─────────────────────────────────────────────────────────────────────────── */
/* Main Entry Point */
/* ─────────────────────────────────────────────────────────────────────────── */
int main(int argc, char *argv[]) {
fprintf(stderr, "[simulator] LoRa KISS Modem Simulator\n");
fprintf(stderr, "[simulator] Reading KISS frames from stdin, writing to stdout\n");
fprintf(stderr, "[simulator] Ctrl+D to exit\n\n");
setup();
int iterations = 0;
while (true) {
loop();
iterations++;
usleep(1000); /* 1ms sleep to avoid busy waiting */
}
return 0;
}