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
This commit is contained in:
204
test/main_simulator.cpp
Normal file
204
test/main_simulator.cpp
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
Reference in New Issue
Block a user