From 07053189540e1cbdb57481ca19688b8ad947116f Mon Sep 17 00:00:00 2001 From: hans boot Date: Fri, 21 Feb 2025 13:10:27 +0100 Subject: [PATCH 01/17] mock PSU, prepare work --- .gitignore | 1 + include/riden_logging/riden_logging.h | 4 +- platformio.ini | 26 ++-------- scripts/test_pyvisa.py | 48 +++++++++++++++++++ src/main.cpp | 10 +++- src/riden_config/riden_config.cpp | 8 ++++ src/riden_modbus/riden_modbus.cpp | 30 ++++++++++++ .../riden_modbus_bridge.cpp | 8 ++++ src/riden_scpi/riden_scpi.cpp | 12 ++++- 9 files changed, 122 insertions(+), 25 deletions(-) create mode 100644 scripts/test_pyvisa.py diff --git a/.gitignore b/.gitignore index 07a5876..6c575ba 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ .venv/ __pycache__ *.pyc +.vscode/extensions.json diff --git a/include/riden_logging/riden_logging.h b/include/riden_logging/riden_logging.h index ac9fb5e..af55839 100644 --- a/include/riden_logging/riden_logging.h +++ b/include/riden_logging/riden_logging.h @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: 2024 Peder Toftegaard Olsen // // SPDX-License-Identifier: MIT - +#ifdef MOCK_RIDEN +#define MODBUS_USE_SOFWARE_SERIAL +#endif #ifdef MODBUS_USE_SOFWARE_SERIAL #include #define LOG(a) Serial.print(a) diff --git a/platformio.ini b/platformio.ini index 0c92b20..dc3410c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -18,6 +18,7 @@ lib_deps = build_flags = -D DEFAULT_UART_BAUDRATE=9600 -D USE_FULL_ERROR_LIST + -D MOCK_RIDEN extra_scripts = pre:scripts/get_version.py @@ -32,25 +33,8 @@ extra_scripts = ${env.extra_scripts} pre:scripts/get_build_time.py scripts/make_gz.py -upload_port = -upload_resetmethod = nodemcu +upload_protocol = esptool +monitor_port = /dev/cu.usbserial-FTHJGC2Z +upload_port = /dev/cu.usbserial-FTHJGC2Z upload_speed = 115200 -monitor_port = /dev/tty.usbserial-11430 -monitor_speed = 74880 - -[env:nodemcuv2] -board = nodemcuv2 -build_flags = - ${env.build_flags} - -D LED_BUILTIN=2 - -D MODBUS_USE_SOFWARE_SERIAL - -D MODBUS_RX=D5 # GPIO 14 - -D MODBUS_TX=D6 # GPIO 15 -# -D WM_DEBUG_LEVEL=DEBUG_DEV -# -D MODBUSRTU_DEBUG -# -D MODBUSIP_DEBUG -upload_port = /dev/cu.SLAB_USBtoUART -upload_resetmethod = nodemcu -upload_speed = 115200 -monitor_port = /dev/cu.SLAB_USBtoUART -monitor_speed = 74880 +monitor_speed = 115200 diff --git a/scripts/test_pyvisa.py b/scripts/test_pyvisa.py new file mode 100644 index 0000000..59182c7 --- /dev/null +++ b/scripts/test_pyvisa.py @@ -0,0 +1,48 @@ +import argparse +import pyvisa + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description="Test simple SCPI communication via VISA.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("port", type=str, nargs='?', default=None, help="The port to use. Must be a Visa compatible connection string.") + parser.add_argument("-n", action="store_true", default=False, help="No scan for test SCPI devices. Will be ignored when port is not defined.") + args = parser.parse_args() + + rm = pyvisa.ResourceManager() + skip_scan = args.n + if not args.port: + skip_scan = False + if not skip_scan: + print("Scanning for VISA resources...") + print("VISA Resources found: ", end='') + resources = rm.list_resources(query="?*") + print(resources) + for m in resources: + if m.startswith("ASRL/dev/cu."): + # some of these devices are not SCPI compatible, like "ASRL/dev/cu.Bluetooth-Incoming-Port::INSTR" + print(f"Skipping serial port \"{m}\"") + continue + try: + inst = rm.open_resource(m, timeout=1000) + r = inst.query("*IDN?").strip() + print(f"Found \"{r}\" on address \"{m}\"") + except: + print(f"Found unknown device on address \"{m}\"") + else: + print("No scan for VISA resources.") + if args.port: + print(f"Connecting to '{args.port}'") + inst = rm.open_resource(args.port, timeout=2000) + if args.port.endswith("::SOCKET"): + inst.read_termination = "\n" + inst.write_termination = "\n" + print("Connected.") + msgs = ["*IDN?"] + for m in msgs: + if m.endswith("?"): + print(f"Query \"{m}\" reply: ", end='') + r = inst.query(m).strip() + print(f"\"{r}\"") + else: + print(f"Write \"{m}\"") + inst.write(m) diff --git a/src/main.cpp b/src/main.cpp index 8a45ab5..0d79e89 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,6 +22,10 @@ #define NTP_SERVER "pool.ntp.org" +#ifdef MOCK_RIDEN +#define MODBUS_USE_SOFWARE_SERIAL +#endif + using namespace RidenDongle; static Ticker led_ticker; @@ -67,7 +71,7 @@ void setup() pinMode(LED_BUILTIN, OUTPUT); led_ticker.attach(0.6, tick); -#if MODBUS_USE_SOFWARE_SERIAL +#ifdef MODBUS_USE_SOFWARE_SERIAL Serial.begin(74880); delay(1000); #endif @@ -132,6 +136,10 @@ static bool connect_wifi(const char *hostname) wifi_connected = wifiManager.autoConnect(hostname); } if (wifi_connected) { + + LOG_F("WiFi SSID: %s\r\n", WiFi.SSID().c_str()); + LOG_F("IP: %s\r\n", WiFi.localIP().toString().c_str()); + experimental::ESP8266WiFiGratuitous::stationKeepAliveSetIntervalMs(); if (hostname != nullptr) { if (!MDNS.begin(hostname)) { diff --git a/src/riden_config/riden_config.cpp b/src/riden_config/riden_config.cpp index 18b8148..9370c1b 100644 --- a/src/riden_config/riden_config.cpp +++ b/src/riden_config/riden_config.cpp @@ -46,6 +46,9 @@ const char *RidenDongle::build_time = nullptr; bool RidenConfig::begin() { +#ifdef MOCK_RIDEN + return true; +#else EEPROM.begin(512); RidenConfigHeader header{0}; EEPROM.get(0, header); @@ -86,6 +89,7 @@ bool RidenConfig::begin() } return success; +#endif } void RidenConfig::set_timezone_name(String tz_name) @@ -146,6 +150,9 @@ void RidenConfig::set_uart_baudrate(uint32_t baudrate) bool RidenConfig::commit() { +#ifdef MOCK_RIDEN + return true; +#else RidenConfigStructV2 config; memcpy(config.header.magic, MAGIC, sizeof(MAGIC)); config.header.config_version = CURRENT_CONFIG_VERSION; @@ -164,6 +171,7 @@ bool RidenConfig::commit() LOG_LN("RidenConfig: Failed to save configuration"); } return success; +#endif } RidenConfig RidenDongle::riden_config; diff --git a/src/riden_modbus/riden_modbus.cpp b/src/riden_modbus/riden_modbus.cpp index deeddfe..92a6918 100644 --- a/src/riden_modbus/riden_modbus.cpp +++ b/src/riden_modbus/riden_modbus.cpp @@ -7,6 +7,7 @@ #include #include +#ifndef MOCK_RIDEN #ifdef MODBUS_USE_SOFWARE_SERIAL #include #endif @@ -18,11 +19,18 @@ SoftwareSerial SerialRuideng = SoftwareSerial(MODBUS_RX, MODBUS_TX); #else #define SerialRuideng Serial #endif +#endif using namespace RidenDongle; bool RidenModbus::begin() { +#ifdef MOCK_RIDEN + LOG_LN("RuidengModbus mocked"); + initialized = true; + this->type = "RDMOCKED"; + return true; +#else if (initialized) { return true; } @@ -80,16 +88,21 @@ bool RidenModbus::begin() LOG_LN("RuidengModbus initialized"); initialized = true; return true; +#endif } bool RidenModbus::loop() { +#ifdef MOCK_RIDEN + return true; +#else if (!initialized) { return false; } modbus.task(); return true; +#endif } bool RidenModbus::is_connected() @@ -650,6 +663,9 @@ bool RidenModbus::write_boolean(const Register reg, const boolean b) bool RidenModbus::wait_for_inactive() { +#ifdef MOCK_RIDEN + return true; +#else if (!initialized) { return false; } @@ -665,10 +681,15 @@ bool RidenModbus::wait_for_inactive() } } return true; +#endif } bool RidenModbus::read_holding_registers(const uint16_t offset, uint16_t *value, const uint16_t numregs) { +#ifdef MOCK_RIDEN + memset(value, 0, numregs * sizeof(uint16_t)); + return true; +#else if (!wait_for_inactive()) { return false; } @@ -678,10 +699,14 @@ bool RidenModbus::read_holding_registers(const uint16_t offset, uint16_t *value, } // Wait until we receive an answer return wait_for_inactive(); +#endif } bool RidenModbus::write_holding_register(const uint16_t offset, const uint16_t value) { +#ifdef MOCK_RIDEN + return true; +#else if (!wait_for_inactive()) { return false; } @@ -691,10 +716,14 @@ bool RidenModbus::write_holding_register(const uint16_t offset, const uint16_t v } // Wait until we receive an answer return wait_for_inactive(); +#endif } bool RidenModbus::write_holding_registers(const uint16_t offset, uint16_t *value, uint16_t numregs) { +#ifdef MOCK_RIDEN + return true; +#else if (!wait_for_inactive()) { return false; } @@ -704,6 +733,7 @@ bool RidenModbus::write_holding_registers(const uint16_t offset, uint16_t *value } // Wait until we receive an answer return wait_for_inactive(); +#endif } bool RidenModbus::read_holding_registers(const Register reg, uint16_t *value, const uint16_t numregs) diff --git a/src/riden_modbus_bridge/riden_modbus_bridge.cpp b/src/riden_modbus_bridge/riden_modbus_bridge.cpp index 8feb805..4493bce 100644 --- a/src/riden_modbus_bridge/riden_modbus_bridge.cpp +++ b/src/riden_modbus_bridge/riden_modbus_bridge.cpp @@ -76,6 +76,9 @@ Modbus::ResultCode RidenModbusBridge::modbus_tcp_raw_callback(uint8_t *data, uin return Modbus::EX_GENERAL_FAILURE; } // Wait until no transaction is active +#ifdef MOCK_RIDEN + return Modbus::EX_SUCCESS; +#else while (riden_modbus.modbus.server()) { delay(1); riden_modbus.modbus.task(); @@ -94,6 +97,7 @@ Modbus::ResultCode RidenModbusBridge::modbus_tcp_raw_callback(uint8_t *data, uin ip = source->ipaddr; riden_modbus.modbus.onRaw(::modbus_rtu_raw_callback); return Modbus::EX_SUCCESS; // Stops ModbusTCP from processing the data +#endif } /** @@ -105,6 +109,9 @@ Modbus::ResultCode RidenModbusBridge::modbus_rtu_raw_callback(uint8_t *data, uin if (!initialized) { return Modbus::EX_GENERAL_FAILURE; } +#ifdef MOCK_RIDEN + return Modbus::EX_SUCCESS; +#else // Stop intercepting raw data riden_modbus.modbus.onRaw(nullptr); @@ -122,6 +129,7 @@ Modbus::ResultCode RidenModbusBridge::modbus_rtu_raw_callback(uint8_t *data, uin slave_id = 0; ip = 0; return Modbus::EX_SUCCESS; // Stops ModbusRTU from processing the data +#endif } Modbus::ResultCode modbus_tcp_raw_callback(uint8_t *data, uint8_t len, void *custom_data) diff --git a/src/riden_scpi/riden_scpi.cpp b/src/riden_scpi/riden_scpi.cpp index 9927e73..d30a186 100644 --- a/src/riden_scpi/riden_scpi.cpp +++ b/src/riden_scpi/riden_scpi.cpp @@ -11,6 +11,10 @@ #include #include +#ifdef MOCK_RIDEN +#define MODBUS_USE_SOFWARE_SERIAL +#endif + using namespace RidenDongle; // We only support one client @@ -121,6 +125,7 @@ size_t SCPI_ResultChoice(scpi_t *context, scpi_choice_def_t *options, int32_t va size_t RidenScpi::SCPI_Write(scpi_t *context, const char *data, size_t len) { + // TODO adapt to hislip RidenScpi *ridenScpi = static_cast(context->user_context); memcpy(&(ridenScpi->write_buffer[ridenScpi->write_buffer_length]), data, len); @@ -131,6 +136,7 @@ size_t RidenScpi::SCPI_Write(scpi_t *context, const char *data, size_t len) scpi_result_t RidenScpi::SCPI_Flush(scpi_t *context) { + // TODO adapt to hislip RidenScpi *ridenScpi = static_cast(context->user_context); if (ridenScpi->client) { @@ -639,11 +645,12 @@ scpi_result_t RidenScpi::SystemBeeperStateQ(scpi_t *context) bool RidenScpi::begin() { + // TODO adapt to hislip if (initialized) { return true; } - LOG_LN("RidenScpi initializing"); + LOG_LN("RidenScpiRaw initializing"); String type = ridenModbus.get_type(); uint32_t serial_number; @@ -672,7 +679,7 @@ bool RidenScpi::begin() MDNS.addServiceTxt(scpi_service, "version", SCPI_STD_VERSION_REVISION); } - LOG_LN("RidenScpi initialized"); + LOG_LN("RidenScpiRaw initialized"); initialized = true; return true; @@ -680,6 +687,7 @@ bool RidenScpi::begin() bool RidenScpi::loop() { + // TODO adapt to hislip // Check for new client connecting WiFiClient newClient = tcpServer.accept(); if (newClient) { From 60c45105f51de6e629b4e7ac6255f2ae99ca3117 Mon Sep 17 00:00:00 2001 From: hans boot Date: Fri, 21 Feb 2025 22:37:49 +0100 Subject: [PATCH 02/17] basis for work on vxi-11 --- .gitignore | 1 + include/riden_scpi/riden_scpi.h | 10 ++ platformio.ini | 3 +- src/riden_http_server/riden_http_server.cpp | 4 +- src/riden_scpi/riden_scpi.cpp | 112 ++++++++++++++++++-- 5 files changed, 117 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 6c575ba..85059e7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__ *.pyc .vscode/extensions.json +.vscode/settings.json diff --git a/include/riden_scpi/riden_scpi.h b/include/riden_scpi/riden_scpi.h index e3c2406..57e46ef 100644 --- a/include/riden_scpi/riden_scpi.h +++ b/include/riden_scpi/riden_scpi.h @@ -13,7 +13,13 @@ #define WRITE_BUFFER_LENGTH (256) #define SCPI_INPUT_BUFFER_LENGTH 256 #define SCPI_ERROR_QUEUE_SIZE 17 +#if defined(USE_HISLIP) +#define DEFAULT_SCPI_PORT 4880 +#elif defined(USE_VXI11) +#define DEFAULT_SCPI_PORT 1024 +#else #define DEFAULT_SCPI_PORT 5025 +#endif namespace RidenDongle { @@ -30,6 +36,8 @@ class RidenScpi std::list get_connected_clients(); void disconnect_client(const IPAddress &ip); + const char *get_visa_resource(); + private: RidenModbus &ridenModbus; @@ -60,6 +68,8 @@ class RidenScpi // conventions. static size_t SCPI_Write(scpi_t *context, const char *data, size_t len); static scpi_result_t SCPI_Flush(scpi_t *context); + scpi_result_t SCPI_FlushRaw(void); + static int SCPI_Error(scpi_t *context, int_fast16_t err); static scpi_result_t SCPI_Control(scpi_t *context, scpi_ctrl_name_t ctrl, scpi_reg_val_t val); static scpi_result_t SCPI_Reset(scpi_t *context); diff --git a/platformio.ini b/platformio.ini index dc3410c..c3e77bd 100644 --- a/platformio.ini +++ b/platformio.ini @@ -19,6 +19,7 @@ build_flags = -D DEFAULT_UART_BAUDRATE=9600 -D USE_FULL_ERROR_LIST -D MOCK_RIDEN + -D USE_VXI11 extra_scripts = pre:scripts/get_version.py @@ -37,4 +38,4 @@ upload_protocol = esptool monitor_port = /dev/cu.usbserial-FTHJGC2Z upload_port = /dev/cu.usbserial-FTHJGC2Z upload_speed = 115200 -monitor_speed = 115200 +monitor_speed = 74880 diff --git a/src/riden_http_server/riden_http_server.cpp b/src/riden_http_server/riden_http_server.cpp index cbe4e84..97189e4 100644 --- a/src/riden_http_server/riden_http_server.cpp +++ b/src/riden_http_server/riden_http_server.cpp @@ -628,7 +628,5 @@ const char *RidenHttpServer::get_serial_number() const char *RidenHttpServer::get_visa_resource() { - static char visa_resource[40]; - sprintf(visa_resource, "TCPIP::%s::%u::SOCKET", WiFi.localIP().toString().c_str(), scpi.port()); - return visa_resource; + return scpi.get_visa_resource(); } diff --git a/src/riden_scpi/riden_scpi.cpp b/src/riden_scpi/riden_scpi.cpp index d30a186..a3e8d6a 100644 --- a/src/riden_scpi/riden_scpi.cpp +++ b/src/riden_scpi/riden_scpi.cpp @@ -6,6 +6,11 @@ #include #include +#ifdef USE_HISLIP +//#include +#endif +// TODO: add vxi11 support + #include #include #include @@ -125,7 +130,6 @@ size_t SCPI_ResultChoice(scpi_t *context, scpi_choice_def_t *options, int32_t va size_t RidenScpi::SCPI_Write(scpi_t *context, const char *data, size_t len) { - // TODO adapt to hislip RidenScpi *ridenScpi = static_cast(context->user_context); memcpy(&(ridenScpi->write_buffer[ridenScpi->write_buffer_length]), data, len); @@ -136,13 +140,16 @@ size_t RidenScpi::SCPI_Write(scpi_t *context, const char *data, size_t len) scpi_result_t RidenScpi::SCPI_Flush(scpi_t *context) { - // TODO adapt to hislip RidenScpi *ridenScpi = static_cast(context->user_context); + return ridenScpi->SCPI_FlushRaw(); +} - if (ridenScpi->client) { - ridenScpi->client.write(ridenScpi->write_buffer, ridenScpi->write_buffer_length); - ridenScpi->write_buffer_length = 0; - ridenScpi->client.flush(); +scpi_result_t RidenScpi::SCPI_FlushRaw(void) +{ + if (client) { + client.write(write_buffer, write_buffer_length); + write_buffer_length = 0; + client.flush(); } return SCPI_RES_OK; @@ -650,7 +657,7 @@ bool RidenScpi::begin() return true; } - LOG_LN("RidenScpiRaw initializing"); + LOG_LN("RidenScpi initializing"); String type = ridenModbus.get_type(); uint32_t serial_number; @@ -675,11 +682,22 @@ bool RidenScpi::begin() tcpServer.setNoDelay(true); if (MDNS.isRunning()) { +#if defined(USE_HISLIP) + LOG_LN("RidenScpi advertising as hislip."); + auto scpi_service = MDNS.addService(NULL, "hislip", "tcp", tcpServer.port()); + MDNS.addServiceTxt(scpi_service, "version", SCPI_STD_VERSION_REVISION); +#elif defined(USE_VXI11) + LOG_LN("RidenScpi advertising as vxi-11."); + auto scpi_service = MDNS.addService(NULL, "vxi-11", "tcp", tcpServer.port()); + MDNS.addServiceTxt(scpi_service, "version", SCPI_STD_VERSION_REVISION); +#else + LOG_LN("RidenScpi advertising as scpi-raw."); auto scpi_service = MDNS.addService(NULL, "scpi-raw", "tcp", tcpServer.port()); MDNS.addServiceTxt(scpi_service, "version", SCPI_STD_VERSION_REVISION); +#endif } - LOG_LN("RidenScpiRaw initialized"); + LOG_LN("RidenScpi initialized"); initialized = true; return true; @@ -687,10 +705,11 @@ bool RidenScpi::begin() bool RidenScpi::loop() { - // TODO adapt to hislip + // TODO This does not work with hislip, where we need 2 connections at the same time // Check for new client connecting WiFiClient newClient = tcpServer.accept(); if (newClient) { + LOG_LN("RidenScpi: New client."); if (!client) { newClient.setTimeout(100); newClient.setNoDelay(true); @@ -711,10 +730,42 @@ bool RidenScpi::loop() if (bytes_read > 0) { scpi_context.buffer.position += bytes_read; scpi_context.buffer.length += bytes_read; +#if defined(USE_HISLIP) +#error "HiSLIP is not yet supported" + write_buffer_length = 0; + int rv = hs_process_data(scpi_context.buffer.data, scpi_context.buffer.length, write_buffer, &write_buffer_length, sizeof(write_buffer)); + + LOG_F("process return: %d, out buffer size: %u\n", rv, write_buffer_length); + switch(rv) + { + case 2: + // We have a complete message to be sent immediately + SCPI_FlushRaw(); + scpi_context.buffer.position = 0; + scpi_context.buffer.length = 0; + break; + case 1: + // TODO we have a complete payload + scpi_context.buffer.position = 0; + scpi_context.buffer.length = 0; + break; + case 0: + // we need more data + break; + default: + scpi_context.buffer.position = 0; + scpi_context.buffer.length = 0; + break; + } +#elif defined(USE_VXI11) + // TODO implement VXI-11 +#error "VXI-11 is not yet supported" +#else uint8_t last_byte = scpi_context.buffer.data[scpi_context.buffer.position - 1]; if (last_byte == '\n') { SCPI_Input(&scpi_context, NULL, 0); } +#endif } } else { // Client is sending more data than we can handle @@ -725,6 +776,7 @@ bool RidenScpi::loop() // Stop client which disconnects if (client && !client.connected()) { + LOG_LN("RidenScpi: disconnect client."); client.stop(); } @@ -758,3 +810,45 @@ void RidenScpi::reset_buffers() scpi_context.buffer.length = 0; scpi_context.buffer.position = 0; } + +const char *RidenScpi::get_visa_resource() +{ + static char visa_resource[40]; +#if defined(USE_HISLIP) + sprintf(visa_resource, "TCPIP::%s::hislip0,%u::INSTR", WiFi.localIP().toString().c_str(), port()); +#elif defined(USE_VXI11) + sprintf(visa_resource, "TCPIP::%s::INSTR", WiFi.localIP().toString().c_str()); +#else + sprintf(visa_resource, "TCPIP::%s::%u::SOCKET", WiFi.localIP().toString().c_str(), port()); +#endif + return visa_resource; +} + + +// ************ +// RAW socket +// scpi-raw uses a raw TCP connection to send and receive SCPI commands. +// This FW implementation supports only 1 client. +// - Discovery is done via mDNS, and the service name is "scpi-raw" (_scpi-raw._tcp). +// - The VISA string is like: "TCPIP::::5025::SOCKET" (using the default port 5025) +// - The SCPI commands and responses are sent as plain text, delimited by newline characters. +// +// HiSLIP +// see https://www.ivifoundation.org/downloads/Protocol%20Specifications/IVI-6.1_HiSLIP-2.0-2020-04-23.pdf +// see https://lxistandard.org/members/Adopted%20Specifications/Latest%20Version%20of%20Standards_/LXI%20Version%201.6/LXI_HiSLIP_Extended_Function_1.3_2022-05-26.pdf +// is a more modern protocol. It can use a synchronous and/or an asynchronous connection. +// - Discovery is done via mDNS, and the service name is "hislip" (_hislip._tcp) +// - The VISA string is like: "TCPIP::::hislip0::INSTR" (using the default port 4880) +// - The SCPI commands and responses are sent as binary data, with a header and a payload. +// - It requires 2 connections on the same port (async and sync), even if you only use 1 +// ==> NOT EASY TO DO WITHOUT REWRITING MUCH OF riden_scpi.cpp +// +// VXI-11 +// widely supported +// - Requires 2 socket services: portmap/rpcbind (port 111) and vxi-11 (any port you want) +// - Discovery is done via portmap when replying to GETPORT VXI-11 Core. This should be on UDP and TCP, but you can get by in TCP. +// - Secondary discovery is done via mDNS, and the service name is "vxi-11" (_vxi-11._tcp). This however still requires the portmapper. +// - The VISA string is like: "TCPIP::::INSTR" +// - The SCPI commands and responses are sent as binary data, with a header and a payload. +// - VXI-11 has separate commands for reading and writing. +// - TODO: investigate mdns with _vxi-11._tcp From eda24fb92f54e7faa703e7a535fd4960a9c8bccc Mon Sep 17 00:00:00 2001 From: hans boot Date: Fri, 21 Feb 2025 22:51:26 +0100 Subject: [PATCH 03/17] better doc --- .gitignore | 2 + src/riden_scpi/riden_scpi.cpp | 74 +++++++++++++++++++++-------------- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 85059e7..f91c15d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ __pycache__ *.pyc .vscode/extensions.json .vscode/settings.json +src/hislip_server/ +pcap/ diff --git a/src/riden_scpi/riden_scpi.cpp b/src/riden_scpi/riden_scpi.cpp index a3e8d6a..707b1b3 100644 --- a/src/riden_scpi/riden_scpi.cpp +++ b/src/riden_scpi/riden_scpi.cpp @@ -16,6 +16,49 @@ #include #include +// ************ +// This file combines a socket server with an SCPI parser. +// +// 3 different types of socket servers are supported or are potentially possible: +// +// ** RAW socket +// The default. Requires no special flags. +// scpi-raw uses a raw TCP connection to send and receive SCPI commands. +// This FW implementation supports only 1 client. +// - Discovery is done via mDNS, and the service name is "scpi-raw" (_scpi-raw._tcp). +// - The VISA string is like: "TCPIP::::5025::SOCKET" (using the default port 5025) +// - The SCPI commands and responses are sent as plain text, delimited by newline characters. +// - is not discoverable by pyvisa, and requires a construction like this in Python: +// if args.port.endswith("::SOCKET"): +// inst.read_termination = "\n" +// inst.write_termination = "\n" +// +// +// ** VXI-11 +// Requires -D USE_VXI11 +// widely supported +// - Requires 2 socket services: portmap/rpcbind (port 111) and vxi-11 (any port you want) +// - Discovery is done via portmap when replying to GETPORT VXI-11 Core. This should be on UDP and TCP, but you can get by in TCP. +// - Secondary discovery is done via mDNS, and the service name is "vxi-11" (_vxi-11._tcp). This however still requires the portmapper. +// - The VISA string is like: "TCPIP::::INSTR" +// - The SCPI commands and responses are sent as binary data, with a header and a payload. +// - VXI-11 has separate commands for reading and writing. +// - is discoverable by pyvisa, and requires no special construction in Python +// +// +// ** HiSLIP +// Requires -D USE_HISLIP +// see https://www.ivifoundation.org/downloads/Protocol%20Specifications/IVI-6.1_HiSLIP-2.0-2020-04-23.pdf +// see https://lxistandard.org/members/Adopted%20Specifications/Latest%20Version%20of%20Standards_/LXI%20Version%201.6/LXI_HiSLIP_Extended_Function_1.3_2022-05-26.pdf +// is a more modern protocol. It can use a synchronous and/or an asynchronous connection. +// - Discovery is done via mDNS, and the service name is "hislip" (_hislip._tcp) +// - The VISA string is like: "TCPIP::::hislip0::INSTR" (using the default port 4880) +// - The SCPI commands and responses are sent as binary data, with a header and a payload. +// - It requires 2 connections on the same port (async and sync), even if you only use 1 +// - is discoverable by pyvisa, and requires no special construction in Python other than installation of zeroconf +// ==> THE 2 CONNECTIONS MAKE IT NOT EASY TO DO WITHOUT REWRITING MUCH OF riden_scpi.cpp, AND WORK WAS ABANDONED + + #ifdef MOCK_RIDEN #define MODBUS_USE_SOFWARE_SERIAL #endif @@ -731,7 +774,7 @@ bool RidenScpi::loop() scpi_context.buffer.position += bytes_read; scpi_context.buffer.length += bytes_read; #if defined(USE_HISLIP) -#error "HiSLIP is not yet supported" +#error "HiSLIP is not yet supported, it requires 2 clients at the same time" write_buffer_length = 0; int rv = hs_process_data(scpi_context.buffer.data, scpi_context.buffer.length, write_buffer, &write_buffer_length, sizeof(write_buffer)); @@ -759,7 +802,7 @@ bool RidenScpi::loop() } #elif defined(USE_VXI11) // TODO implement VXI-11 -#error "VXI-11 is not yet supported" +#error "VXI-11 is not yet supported, WIP" #else uint8_t last_byte = scpi_context.buffer.data[scpi_context.buffer.position - 1]; if (last_byte == '\n') { @@ -825,30 +868,3 @@ const char *RidenScpi::get_visa_resource() } -// ************ -// RAW socket -// scpi-raw uses a raw TCP connection to send and receive SCPI commands. -// This FW implementation supports only 1 client. -// - Discovery is done via mDNS, and the service name is "scpi-raw" (_scpi-raw._tcp). -// - The VISA string is like: "TCPIP::::5025::SOCKET" (using the default port 5025) -// - The SCPI commands and responses are sent as plain text, delimited by newline characters. -// -// HiSLIP -// see https://www.ivifoundation.org/downloads/Protocol%20Specifications/IVI-6.1_HiSLIP-2.0-2020-04-23.pdf -// see https://lxistandard.org/members/Adopted%20Specifications/Latest%20Version%20of%20Standards_/LXI%20Version%201.6/LXI_HiSLIP_Extended_Function_1.3_2022-05-26.pdf -// is a more modern protocol. It can use a synchronous and/or an asynchronous connection. -// - Discovery is done via mDNS, and the service name is "hislip" (_hislip._tcp) -// - The VISA string is like: "TCPIP::::hislip0::INSTR" (using the default port 4880) -// - The SCPI commands and responses are sent as binary data, with a header and a payload. -// - It requires 2 connections on the same port (async and sync), even if you only use 1 -// ==> NOT EASY TO DO WITHOUT REWRITING MUCH OF riden_scpi.cpp -// -// VXI-11 -// widely supported -// - Requires 2 socket services: portmap/rpcbind (port 111) and vxi-11 (any port you want) -// - Discovery is done via portmap when replying to GETPORT VXI-11 Core. This should be on UDP and TCP, but you can get by in TCP. -// - Secondary discovery is done via mDNS, and the service name is "vxi-11" (_vxi-11._tcp). This however still requires the portmapper. -// - The VISA string is like: "TCPIP::::INSTR" -// - The SCPI commands and responses are sent as binary data, with a header and a payload. -// - VXI-11 has separate commands for reading and writing. -// - TODO: investigate mdns with _vxi-11._tcp From 91fc1fef167b7aca80807791f55de6ac0d160df0 Mon Sep 17 00:00:00 2001 From: hans boot Date: Sat, 22 Feb 2025 19:37:11 +0100 Subject: [PATCH 04/17] Independent vxi server is up, integration TODO --- include/riden_logging/riden_logging.h | 2 + include/riden_scpi/riden_scpi.h | 2 - src/main.cpp | 9 + src/riden_scpi/riden_scpi.cpp | 10 - src/vxi11_server/README.md | 45 +++ src/vxi11_server/rpc_bind_server.cpp | 123 ++++++++ src/vxi11_server/rpc_bind_server.h | 67 +++++ src/vxi11_server/rpc_enums.h | 148 ++++++++++ src/vxi11_server/rpc_packets.cpp | 210 ++++++++++++++ src/vxi11_server/rpc_packets.h | 390 ++++++++++++++++++++++++++ src/vxi11_server/utilities.h | 177 ++++++++++++ src/vxi11_server/vxi_server.cpp | 210 ++++++++++++++ src/vxi11_server/vxi_server.h | 58 ++++ src/vxi11_server/wifi_ext.h | 38 +++ 14 files changed, 1477 insertions(+), 12 deletions(-) create mode 100644 src/vxi11_server/README.md create mode 100644 src/vxi11_server/rpc_bind_server.cpp create mode 100644 src/vxi11_server/rpc_bind_server.h create mode 100644 src/vxi11_server/rpc_enums.h create mode 100644 src/vxi11_server/rpc_packets.cpp create mode 100644 src/vxi11_server/rpc_packets.h create mode 100644 src/vxi11_server/utilities.h create mode 100644 src/vxi11_server/vxi_server.cpp create mode 100644 src/vxi11_server/vxi_server.h create mode 100644 src/vxi11_server/wifi_ext.h diff --git a/include/riden_logging/riden_logging.h b/include/riden_logging/riden_logging.h index af55839..7409123 100644 --- a/include/riden_logging/riden_logging.h +++ b/include/riden_logging/riden_logging.h @@ -9,8 +9,10 @@ #define LOG(a) Serial.print(a) #define LOG_LN(a) Serial.println(a) #define LOG_F(...) Serial.printf(__VA_ARGS__) +#define LOG_DUMP(buf,len) for (size_t i = 0; i < len; i++) { LOG_F("%02X ", buf[i]); } #else #define LOG(a) #define LOG_LN(a) #define LOG_F(...) +#define LOG_DUMP(buf,len) #endif diff --git a/include/riden_scpi/riden_scpi.h b/include/riden_scpi/riden_scpi.h index 57e46ef..b9a5c69 100644 --- a/include/riden_scpi/riden_scpi.h +++ b/include/riden_scpi/riden_scpi.h @@ -15,8 +15,6 @@ #define SCPI_ERROR_QUEUE_SIZE 17 #if defined(USE_HISLIP) #define DEFAULT_SCPI_PORT 4880 -#elif defined(USE_VXI11) -#define DEFAULT_SCPI_PORT 1024 #else #define DEFAULT_SCPI_PORT 5025 #endif diff --git a/src/main.cpp b/src/main.cpp index 0d79e89..6bdbaa0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #include @@ -40,6 +42,9 @@ static RidenModbus riden_modbus; static RidenScpi riden_scpi(riden_modbus); static RidenModbusBridge modbus_bridge(riden_modbus); static RidenHttpServer http_server(riden_modbus, riden_scpi, modbus_bridge); +static SCPI_handler scpi_handler; +static VXI_Server vxi_server(scpi_handler); ///< The VXI_Server +static RPC_Bind_Server rpc_bind_server(vxi_server); ///< The RPC_Bind_Server for the vxi server /** * Invoked by led_ticker to flash the LED. @@ -98,6 +103,8 @@ void setup() riden_scpi.begin(); modbus_bridge.begin(); + vxi_server.begin(); + rpc_bind_server.begin(); // turn off led led_ticker.detach(); @@ -188,6 +195,8 @@ void loop() riden_modbus.loop(); riden_scpi.loop(); modbus_bridge.loop(); + rpc_bind_server.loop(); + vxi_server.loop(); } http_server.loop(); ArduinoOTA.handle(); diff --git a/src/riden_scpi/riden_scpi.cpp b/src/riden_scpi/riden_scpi.cpp index 707b1b3..f150380 100644 --- a/src/riden_scpi/riden_scpi.cpp +++ b/src/riden_scpi/riden_scpi.cpp @@ -9,7 +9,6 @@ #ifdef USE_HISLIP //#include #endif -// TODO: add vxi11 support #include #include @@ -729,10 +728,6 @@ bool RidenScpi::begin() LOG_LN("RidenScpi advertising as hislip."); auto scpi_service = MDNS.addService(NULL, "hislip", "tcp", tcpServer.port()); MDNS.addServiceTxt(scpi_service, "version", SCPI_STD_VERSION_REVISION); -#elif defined(USE_VXI11) - LOG_LN("RidenScpi advertising as vxi-11."); - auto scpi_service = MDNS.addService(NULL, "vxi-11", "tcp", tcpServer.port()); - MDNS.addServiceTxt(scpi_service, "version", SCPI_STD_VERSION_REVISION); #else LOG_LN("RidenScpi advertising as scpi-raw."); auto scpi_service = MDNS.addService(NULL, "scpi-raw", "tcp", tcpServer.port()); @@ -800,9 +795,6 @@ bool RidenScpi::loop() scpi_context.buffer.length = 0; break; } -#elif defined(USE_VXI11) - // TODO implement VXI-11 -#error "VXI-11 is not yet supported, WIP" #else uint8_t last_byte = scpi_context.buffer.data[scpi_context.buffer.position - 1]; if (last_byte == '\n') { @@ -859,8 +851,6 @@ const char *RidenScpi::get_visa_resource() static char visa_resource[40]; #if defined(USE_HISLIP) sprintf(visa_resource, "TCPIP::%s::hislip0,%u::INSTR", WiFi.localIP().toString().c_str(), port()); -#elif defined(USE_VXI11) - sprintf(visa_resource, "TCPIP::%s::INSTR", WiFi.localIP().toString().c_str()); #else sprintf(visa_resource, "TCPIP::%s::%u::SOCKET", WiFi.localIP().toString().c_str(), port()); #endif diff --git a/src/vxi11_server/README.md b/src/vxi11_server/README.md new file mode 100644 index 0000000..7131e56 --- /dev/null +++ b/src/vxi11_server/README.md @@ -0,0 +1,45 @@ +# VXI handler + +## Credit where credit due + +This code is heavily inspired by work from https://github.com/awakephd/espBode + +It is based on an extract from 2025-02-22. + +Changes made: + +* made platformio compatible (removed the streams) +* removed unneeded parts, simplified, compacted style +* created interfaces to riden_scpi elsewhere in this project + +# About VXI-11 + +A VXI-11 setup is normally built upon 3 socket services: + +* portmap/rpcbind (port 111, on UDP and TCP) +* vxi-11 (any TCP port you want) + +Discovery is done via portmap when replying to GETPORT VXI-11 Core. It replies with a port number, normally taken out of a range of ports. + +This should be on UDP and TCP, but you can get by in TCP. This implementation supports both. + +Secondary discovery is done via mDNS, and the service name is "vxi-11" (_vxi-11._tcp). This however still requires the portmapper. mDNS is not handled here. + +The VISA string is like: `TCPIP::::INSTR` + +The SCPI commands and responses are sent as binary data, with a header and a payload. + +VXI-11 has separate commands for reading and writing, unlike scpi-raw. + +This type of service is discoverable by pyvisa, and requires no special construction in Python + +# Overall functionning of this code + +* 2 portmap services are started, one on TCP, one on UDP, both ready to handle "PORTMAP" requests. +* 1 vxi server will be started +* The portmap services, upon reception of a PORTMAP request, and only if another client is not already connected to the vxi server, will reply with the port of the vxi server. +* The vxi service handles in essence 4 types of requests: + * VXI_11_CREATE_LINK: accept a new client + * VXI_11_DEV_WRITE: receive a new SCPI request from the client, and send to the SCPI device + * VXI_11_DEV_READ: send any last data the SCPI device created to the client + * VXI_11_DESTROY_LINK: close the connection. It can be forced to restart the vxi server on a new port, taken from a range of ports, as some clients require ports to change at each connection. diff --git a/src/vxi11_server/rpc_bind_server.cpp b/src/vxi11_server/rpc_bind_server.cpp new file mode 100644 index 0000000..69213a5 --- /dev/null +++ b/src/vxi11_server/rpc_bind_server.cpp @@ -0,0 +1,123 @@ +/*! + @file rpc_bind_server.cpp + @brief Defines the methods of the RCP_Bind_Server class. +*/ + +#include "rpc_bind_server.h" +#include "rpc_enums.h" +#include "rpc_packets.h" +#include "vxi_server.h" + +void RPC_Bind_Server::begin() +{ + /* + Initialize the UDP and TCP servers to listen on + the BIND_PORT port. + */ + + udp.begin(rpc::BIND_PORT); + tcp.begin(rpc::BIND_PORT); + + LOG_F("Listening for RPC_BIND requests on UDP and TCP port %d\n", rpc::BIND_PORT); +} + +/*! + The loop() member function should be called by + the main loop of the program to process any UDP or + TCP bind requests. It will only process requests if + the vxi_server is available. If so, it will hand off the + TCP or UDP request to process_request() for validation + and response. The response will be assembled by + process_request(), but it will be sent from loop() since + we know whether to send it via UDP or TCP. +*/ +void RPC_Bind_Server::loop() +{ + /* What to do if the vxi_server is busy? + + There is no "out of resources" error code return from the + RPC BIND request. We could respond with PROC_UNAVAIL, but + that might suggest that the ESP simply cannot do RPC BIND + at all (as opposed to not right now). Another optioon is to + reject the message, but the enumerated reasons for rejection + (RPC_MISMATCH, AUTH_ERROR) do not seem appropriate. For now, + the solution is to ignore (not read) incoming requests until + a vxi_server becomes available. + */ + + if (vxi_server.available()) { + int len; + + if (udp.parsePacket() > 0) { + len = get_bind_packet(udp); + if (len > 0) { + LOG_F("UDP packet received on port %d\n", rpc::BIND_PORT); + process_request(true); + send_bind_packet(udp, sizeof(bind_response_packet)); + } + } else { + WiFiClient tcp_client; + tcp_client = tcp.accept(); + if (tcp_client) { + len = get_bind_packet(tcp_client); + if (len > 0) { + LOG_F("TCP packet received on port %d\n", rpc::BIND_PORT); + process_request(false); + send_bind_packet(tcp_client, sizeof(bind_response_packet)); + } + } + } + } +} + +/*! + @brief Handle the details of processing an incoming request + for both TCP and UDP servers. + + This function checks to see if the incoming request is a valid + PORT_MAP request. It assembles the response including a + success or error code and the port passed by the VXI_Server. + Actually sending the response is handled by the caller. + + @param onUDP Indicates whether the server calling on this + function is UDP or TCP. +*/ +void RPC_Bind_Server::process_request(bool onUDP) +{ + uint32_t rc = rpc::SUCCESS; + uint32_t port = 0; + + rpc_request_packet *rpc_request = (onUDP ? udp_request : tcp_request); + bind_response_packet *bind_response = (onUDP ? udp_bind_response : tcp_bind_response); + + if (rpc_request->program != rpc::PORTMAP) { + rc = rpc::PROG_UNAVAIL; + + LOG_F("ERROR: Invalid program (expected PORTMAP = 0x186A0; received 0x%08x)\n", (uint32_t)(rpc_request->program)); + } else if (rpc_request->procedure != rpc::GET_PORT) { + rc = rpc::PROC_UNAVAIL; + + LOG_F("ERROR: Invalid procedure (expected GET_PORT = 3; received %u)\n", (uint32_t)(rpc_request->procedure)); + } else { + // i.e., if it is a valid PORTMAP request + LOG_F("PORTMAP command received on %s port %d; ", (onUDP ? "UDP" : "TCP"), rpc::BIND_PORT); + + port = vxi_server.allocate(); + + /* The logic in the loop() routine should not allow + the port returned to be zero, since we first checked + to see if the vxi_server was available. However, we are + including the following test just in case. + */ + + if (port == 0) { + rc = rpc::GARBAGE_ARGS; // not really the appropriate response, but we need to signal failure somehow! + LOG_F("ERROR: PORTMAP failed: vxi_server not available.\n"); + } else { + LOG_F("assigned to port %d\n", port); + } + } + + bind_response->rpc_status = rc; + bind_response->vxi_port = port; +} diff --git a/src/vxi11_server/rpc_bind_server.h b/src/vxi11_server/rpc_bind_server.h new file mode 100644 index 0000000..804a332 --- /dev/null +++ b/src/vxi11_server/rpc_bind_server.h @@ -0,0 +1,67 @@ +#pragma once + +/*! + @file rpc_bind_server.h + @brief Declares the RPC_Bind_Server class +*/ + +#include "utilities.h" +#include "vxi_server.h" +#include +#include + + // class VXI_Server; // forward declaration + +/*! + @brief Listens for and responds to PORT_MAP requests. + + The RPC_Bind_Server class listens for incoming PORT_MAP requests + on port 111, both on UDP and TCP. When a request comes in, it asks + the VXI_Server (passed as part of the construction of the class) + for the current port and returns a response accordingly. Note that + the VXI_Server must be constructed before the RPC_Bind_Server. +*/ +class RPC_Bind_Server +{ + + public: + /*! + @brief Constructor's only task is to save a reference + to the VXI_Server. + + @param vs A reference to the VXI_Server + */ + RPC_Bind_Server(VXI_Server &vs) + : vxi_server(vs) + { + } + + /*! + @brief Destructor only needs to stop the listening services. + */ + ~RPC_Bind_Server() + { + udp.stop(); + tcp.stop(); + }; + + /*! + @brief Initializes the RPC_Bind_Server by setting up + the TCP and UDP servers. + */ + void begin(); + + /*! + @brief Call this at least once per main loop to + process any incoming PORT_MAP requests. + */ + void loop(); + + protected: + void process_request(bool onUDP); + + VXI_Server &vxi_server; ///< Reference to the VXI_Server + WiFiUDP udp; ///< UDP server + WiFiServer_ext tcp; ///< TCP server +}; + diff --git a/src/vxi11_server/rpc_enums.h b/src/vxi11_server/rpc_enums.h new file mode 100644 index 0000000..4afd5ff --- /dev/null +++ b/src/vxi11_server/rpc_enums.h @@ -0,0 +1,148 @@ +#pragma once + +/*! + @file rpc_enums.h + @brief Enumerations of RPC and VXI protocol codes (message types, reply status, + error codes, etc.). + + Helpful information on the protocol of the basic RPC packet is available + at https://www.ibm.com/docs/it/aix/7.2?topic=concepts-remote-procedure-call. + For information on the VXI specific protocol, see the VXIbus TCP/IP Instrument + Protocol Specification at https://vxibus.org/specifications.html. +*/ + +/*! + @brief The rpc namespace is used group the RPC/VXI protocol codes. +*/ +namespace rpc +{ + +/*! + @brief Port numbers used in the RPC and VXI communication. + + Bind requests always come in on port 111, via either UDP or TCP. + Some clients (Siglent oscilloscopes for example) require a different port per link; therefore + it is possible to cycle through a block of ports, changing each time VXI_Server + begins listening for a new link request. + Keeping START and END the same will allow for mDNS publication of the VXI_Server port. +*/ +enum ports { + + BIND_PORT = 111, ///< Port to listen on for bind requests + VXI_PORT_START = 9010, ///< Start of a block of ports to use for VXI transactions + VXI_PORT_END = 9010 ///< End of a block of ports to use for VXI transactions +}; + +/*! + @brief Message types are either CALL (request) or REPLY (response). +*/ +enum msg_type { + + CALL = 0, ///< The message contains a CALL (request) + REPLY = 1 ///< The message contains a REPLY (response) +}; + +/*! + @brief Indicates whether the request was accepted or denied. + + Note that messages should be denied only for mismatch of RPC protocol + or problems with authorization - neither of which are used in espBode. + If accepted, messages will generate a response status which can + indicate other types of errors. +*/ +enum reply_state { + + MSG_ACCEPTED = 0, ///< Message has been accepted; its status is indicated in the rpc_status field + MSG_DENIED = 1 ///< Message has been denied +}; + +/*! + @brief Reasons that a message was denied. +*/ +enum reject_status { + + RPC_MISMATCH = 0, ///< Message denied due to mismatch in RPC protocol + AUTH_ERROR = 1 ///< Message denied due to invalid authorization +}; + +/*! + @brief Additional detail if a message is denied due to invalid authorization. +*/ +enum auth_status { + + AUTH_BADCRED = 1, ///< Credentials were given in an invalid format + AUTH_REJECTEDCRED = 2, ///< Credentials were formatted correctly but were rejected + AUTH_BADVERF = 3, ///< Verification was given in an invalid format + AUTH_REJECTEDVERF = 4, ///< Verification was formatted corrrectly but was rejected + AUTH_TOOWEAK = 5 ///< Authorization level is inadequate +}; + +/*! + @brief Response status for accepted messages. + + This status response can indicate success or a limited set of errors. Many + response types will also include another, more detailed, error field. +*/ +enum rpc_status { + + SUCCESS = 0, ///< Request was successfully executed + PROG_UNAVAIL = 1, ///< The requested program is not available + PROG_MISMATCH = 2, ///< There is a mismatch in the program request (perhaps version number??) + PROC_UNAVAIL = 3, ///< The requested procedure is not available + GARBAGE_ARGS = 4 ///< Invalid syntax / format +}; + +/*! + @brief espBode responds only to PORTMAP and VXI_11_CORE programs. +*/ +enum programs { + + PORTMAP = 0x186A0, ///< Request for the port on which the VXI_Server is listening + VXI_11_CORE = 0x607AF ///< Request for a VXI command to be executed +}; + +/*! + @brief espBode provides only GET_PORT and selected VXI_11 procedures. +*/ +enum procedures { + + GET_PORT = 3, ///< Return the port on which the VXI_Server is currently listening + VXI_11_CREATE_LINK = 10, ///< Create a link to handle a series of requests + VXI_11_DEV_WRITE = 11, ///< Write to the AWG + VXI_11_DEV_READ = 12, ///< Read from the AWG + VXI_11_DESTROY_LINK = 23 ///< Destroy the link and cycle to the next port +}; + +/*! + @brief Error codes that can be returned in response to various VXI_11 commands. +*/ +enum errors { + + NO_ERROR = 0, ///< No error = success + SYNTAX_ERROR = 1, ///< Command contains invalid syntax + NOT_ACCESSIBLE = 3, ///< Cannot access the requested function + INVALID_LINK = 4, ///< The link id does not match + PARAMETER_ERROR = 5, ///< Invalid parameter + NO_CHANNEL = 6, ///< Cannot access the device + INVALID_OPERATION = 8, ///< The requested function is not recognized + OUT_OF_RESOURCES = 9, ///< The device has run out of memory or other resources + DEVICE_LOCKED = 11, ///< The device has been locked by another process + NO_LOCK_HELD = 12, ///< The device has not been properly locked + IO_TIMEOUT = 15, ///< The requested data was not sent/received within the specified timeout interval + LOCK_TIMEOUT = 17, ///< Unable to secure a lock on the device within the specified timeout interval + INVALID_ADDRESS = 21, ///< No device exists at the specified address + ABORT = 23, ///< An abort command has come in via another RPC port + DUPLICATE_CHANNEL = 29 ///< This channel is already in use (?) +}; + +/*! + @brief Indicates the reason for ending the read of data. +*/ +enum reasons { + + END = 4, ///< No more data available to read + CHR = 2, ///< Data reached a terminating character supplied in the read request packet + REQCNT = 1 ///< Data reached the maximum count requested +}; + +}; // namespace rpc diff --git a/src/vxi11_server/rpc_packets.cpp b/src/vxi11_server/rpc_packets.cpp new file mode 100644 index 0000000..06dec64 --- /dev/null +++ b/src/vxi11_server/rpc_packets.cpp @@ -0,0 +1,210 @@ +/*! + @file rpc_packets.cpp + @brief Definitions of variables and basic functions + to receive and send RPC/VXI packets. +*/ + +#include "rpc_packets.h" +#include "rpc_enums.h" + +/* The definition of the buffers to hold packet data */ + +uint8_t udp_read_buffer[UDP_READ_SIZE]; // only for udp bind requests +uint8_t udp_send_buffer[UDP_SEND_SIZE]; // only for udp bind responses +uint8_t tcp_read_buffer[TCP_READ_SIZE]; // only for tcp bind requests +uint8_t tcp_send_buffer[TCP_SEND_SIZE]; // only for tcp bind responses +uint8_t vxi_read_buffer[VXI_READ_SIZE]; // only for vxi requests +uint8_t vxi_send_buffer[VXI_SEND_SIZE]; // only for vxi responses + +/*! + @brief Receive an RPC bind request packet via UDP. + + This function is called only when the udp connection has + data available. It reads the data into the udp_read_buffer. + + @param udp The WiFiUDP connection from which to read. + @return The length of data received. +*/ +uint32_t get_bind_packet(WiFiUDP &udp) +{ + uint32_t len = udp.read(udp_request_packet_buffer, UDP_READ_SIZE); + + if (len > 0) { + LOG_F("\nReceived %d bytes from %s: %d\n", len, udp.remoteIP().toString().c_str(), udp.remotePort()); + LOG_DUMP(udp_request_packet_buffer, len) + LOG_F("\n"); + } + + return len; +} + +/*! + @brief Receive an RPC bind request packet via TCP. + + This function is called only when the tcp client has data + available. It reads the data into the tcp_read_buffer. + + @param tcp The WiFiClient connection from which to read. + @return The length of data received. +*/ +uint32_t get_bind_packet(WiFiClient &tcp) +{ + uint32_t len; + + tcp_request_prefix->length = 0; // set the length to zero in case the following read fails + + tcp.readBytes(tcp_request_prefix_buffer, 4); // get the FRAG + LENGTH field + + len = (tcp_request_prefix->length & 0x7fffffff); // mask out the FRAG bit + + if (len > 4) { + len = std::min(len, (uint32_t)(TCP_READ_SIZE - 4)); // do not read more than the buffer can hold + + tcp.readBytes(tcp_request_packet_buffer, len); + LOG_F("\nReceived %d bytes from %s: %d\n", len + 4, tcp.remoteIP().toString().c_str(), tcp.remotePort()); + LOG_DUMP(tcp_request_prefix_buffer, len + 4) + LOG_F("\n"); + } + + return len; +} + +/*! + @brief Receive an RPC/VXI command request packet via TCP. + + This function is called only when the tcp client has data + available. It reads the data into the vxi_read_buffer. + + @param tcp The WiFiClient connection from which to read. + + @return The length of data received. +*/ +uint32_t get_vxi_packet(WiFiClient &tcp) +{ + uint32_t len; + + vxi_request_prefix->length = 0; // set the length to zero in case the following read fails + + tcp.readBytes(vxi_request_prefix_buffer, 4); // get the FRAG + LENGTH field + + len = (vxi_request_prefix->length & 0x7fffffff); // mask out the FRAG bit + + if (len > 4) { + len = std::min(len, (uint32_t)(VXI_READ_SIZE - 4)); // do not read more than the buffer can hold + + tcp.readBytes(vxi_request_packet_buffer, len); + + LOG_F("\nReceived %d bytes from %s: %d\n", len + 4, tcp.remoteIP().toString().c_str(), tcp.remotePort()); + LOG_DUMP(vxi_request_prefix_buffer, len + 4) + LOG_F("\n"); + } + + return len; +} + +/*! + @brief Send an RPC bind response packet via UDP. + + This function is called to return the port number on which + the VXI_Server is listening. It uses the udp_send_buffer. + + @param udp The udp connection on which to send. + @param len The length of the response to send. +*/ +void send_bind_packet(WiFiUDP &udp, uint32_t len) +{ + fill_response_header(udp_response_packet_buffer, udp_request->xid); // get the xid from the request + + udp.beginPacket(udp.remoteIP(), udp.remotePort()); + udp.write(udp_response_packet_buffer, len); + udp.endPacket(); + + LOG_F("\nSent %d bytes to %s:%d\n", len, udp.remoteIP().toString().c_str(), udp.remotePort()); + LOG_DUMP(udp_response_packet_buffer, len) + LOG_F("\n"); +} + +/*! + @brief Send an RPC bind response packet via TCP. + + This function is called to return the port number on which + the VXI_Server is listening. It uses the tcp_send_buffer. + + @param tcp The WiFiClient to which to send. + @param len The length of the response to send. +*/ +void send_bind_packet(WiFiClient &tcp, uint32_t len) +{ + fill_response_header(tcp_response_packet_buffer, tcp_request->xid); // get the xid from the request + + // adjust length to multiple of 4, appending 0's to fill the dword + + while ((len & 3) > 0) { + tcp_response_packet_buffer[len++] = 0; + } + + tcp_response_prefix->length = 0x80000000 | len; // set the FRAG bit and the length; + + while (tcp.availableForWrite() == 0) + ; // wait for tcp to be available + + tcp.write(tcp_response_prefix_buffer, len + 4); // add 4 to the length to account for the tcp_response_prefix + + LOG_F("\nSent %d bytes to %s:%d\n", len, tcp.remoteIP().toString().c_str(), tcp.remotePort()); + LOG_DUMP(tcp_response_prefix_buffer, len + 4) + LOG_F("\n"); +} + +/*! + @brief Send a VXI command response packet via TCP. + + This function is called to return the response to the + previous command request; the packet includes at least + the basic response header plus an error code, but may + include additional data as appropriate. It uses the + vxi_send_buffer. + + @param tcp The WiFiClient to which to send. + @param len The length of the response to send. +*/ +void send_vxi_packet(WiFiClient &tcp, uint32_t len) +{ + fill_response_header(vxi_response_packet_buffer, vxi_request->xid); + + // adjust length to multiple of 4, appending 0's to fill the dword + + while ((len & 3) > 0) { + vxi_response_packet_buffer[len++] = 0; + } + + vxi_response_prefix->length = 0x80000000 | len; // set the FRAG bit and the length; + + while (tcp.availableForWrite() == 0) + ; // wait for tcp to be available + + tcp.write(vxi_response_prefix_buffer, len + 4); // add 4 to the length to account for the vxi_response_prefix + + LOG_F("\nSent %d bytes to %s:%d\n", len, tcp.remoteIP().toString().c_str(), tcp.remotePort()); + LOG_DUMP(vxi_response_prefix_buffer, len + 4) + LOG_F("\n"); +} + +/*! + @brief Fill in the standard response header data. + + This function is called by the various send_XYZ functions + to fill in the "generic" parts of the response. + + @param buffer The buffer to use for the response + @param xid The transaction id copied from the request +*/ +void fill_response_header(uint8_t *buffer, uint32_t xid) +{ + rpc_response_packet *rpc_response = (rpc_response_packet *)buffer; + + rpc_response->xid = xid; // transaction id supplied by the request + rpc_response->msg_type = rpc::REPLY; // CALL = 0; REPLY = 1 + rpc_response->reply_state = rpc::MSG_ACCEPTED; // MSG_ACCEPTED = 0; MSG_DENIED = 1 + rpc_response->verifier_l = 0; + rpc_response->verifier_h = 0; +} diff --git a/src/vxi11_server/rpc_packets.h b/src/vxi11_server/rpc_packets.h new file mode 100644 index 0000000..345f4d7 --- /dev/null +++ b/src/vxi11_server/rpc_packets.h @@ -0,0 +1,390 @@ +#pragma once + +/*! + @file rpc_packets.h + @brief Declaration of data structures and basic functions to + receive and send RPC/VXI packets. +*/ + +#include "utilities.h" +#include +#include + +/* The get functions take the connection (UDP or TCP client), + read the available data, and return the length of data + received and stored in the data_buffer. +*/ + +uint32_t get_bind_packet(WiFiUDP &udp); +uint32_t get_bind_packet(WiFiClient &tcp); +uint32_t get_vxi_packet(WiFiClient &tcp); + +/* The send functions take the connection (UDP or TCP client) + and the length of the data to send; they send the data + and return void. +*/ + +void send_bind_packet(WiFiUDP &udp, uint32_t len); +void send_bind_packet(WiFiClient &tcp, uint32_t len); +void send_vxi_packet(WiFiClient &tcp, uint32_t len); + +/* The send functions call on fill_response_header to generate + the "generic" data used in all responses. +*/ + +void fill_response_header(uint8_t *buffer, uint32_t xid); + +/*! + @brief Enumeration of the sizes of the various packet buffers. + + The buffers must allow sufficient space to receive the longest + expected data for the type of packet involved. +*/ +enum packet_buffer_sizes { + UDP_READ_SIZE = 64, ///< The UDP bind request should be 56 bytes + UDP_SEND_SIZE = 32, ///< The UDP bind response should be 28 bytes + TCP_READ_SIZE = 64, ///< The TCP bind request should be 56 bytes + 4 bytes for prefix + TCP_SEND_SIZE = 32, ///< The TCP bind response should be 28 bytes + 4 bytes for prefix + VXI_READ_SIZE = 256, ///< The VXI requests should never exceed 128 bytes, but extra allowed + VXI_SEND_SIZE = 256 ///< The VXI responses should never exceed 128 bytes, but extra allowed +}; + +/* declaration of data buffers */ + +extern uint8_t udp_read_buffer[]; ///< Buffer used to receive bind requests via UDP +extern uint8_t udp_send_buffer[]; ///< Buffer used to send bind responses via UDP +extern uint8_t tcp_read_buffer[]; ///< Buffer used to receive bind requests via tcp +extern uint8_t tcp_send_buffer[]; ///< Buffer used to send bind responses via tcp +extern uint8_t vxi_read_buffer[]; ///< Buffer used to receive vxi commands +extern uint8_t vxi_send_buffer[]; ///< Buffer used to send vxi responses + +/* Constants to allow access to the portions of the data_buffers + that represent prefix or packet data for UDP and TCP communication. +*/ + +uint8_t *const udp_request_packet_buffer = udp_read_buffer; ///< The packet portion of a udp bind request +uint8_t *const udp_response_packet_buffer = udp_send_buffer; ///< The packet portion of a udp bind response + +uint8_t *const tcp_request_prefix_buffer = tcp_read_buffer; ///< The prefix portion of a tcp bind request +uint8_t *const tcp_request_packet_buffer = tcp_read_buffer + 4; ///< The packet portion of a tcp bind request + +uint8_t *const tcp_response_prefix_buffer = tcp_send_buffer; ///< The prefix portion of a tcp bind response +uint8_t *const tcp_response_packet_buffer = tcp_send_buffer + 4; ///< The packet portion of a tcp bind response + +uint8_t *const vxi_request_prefix_buffer = vxi_read_buffer; ///< The prefix portion of a vxi command request +uint8_t *const vxi_request_packet_buffer = vxi_read_buffer + 4; ///< The packet portion of a vxi command request + +uint8_t *const vxi_response_prefix_buffer = vxi_send_buffer; ///< The prefix portion of a vxi command response +uint8_t *const vxi_response_packet_buffer = vxi_send_buffer + 4; ///< The packet portion of a vxi command response + +/* Structures to allow description of / access to the data buffers + according to the type of packet. Note that any 32-bit (i.e., non- + character) data is sent and received in big-end format. The + structures below define the data using the big_endian_32_t class, + which "automatically" handles conversion to/from the C++ little-end + uint32_t type. See the rpc_enums.h file for enumeration of the various + message, status, error, and other codes. + + Helpful information on the structure of the basic RPC packet is available + at https://www.ibm.com/docs/it/aix/7.2?topic=concepts-remote-procedure-call. + For information on the VXI specific packets, see the VXIbus TCP/IP Instrument + Protocol Specification at https://vxibus.org/specifications.html. +*/ + +/*! + @brief Structure of an RCP/VXI packet prefix + + TCP communication uses a 4-byte prefix followed by the variable- + length packet. The prefix contains the packet length (not including + the length of the prefix) in the lower 31 bits. The most significant + bit is a flag signalling whether there is more data to come + ("fragment" bit). +*/ +struct tcp_prefix_packet { + big_endian_32_t length; ///< For tcp packets, this prefix contains a FRAG bit (0x80000000) and the length of the following packet +}; + +/*! + @brief Structure of the basic RPC/VXI request packet. + + All RPC/VXI requests will start with the data described + by this structure. Depending on the type of request, there + may be additional data as well. +*/ +struct rpc_request_packet { + big_endian_32_t xid; ///< Transaction id (should be checked to make sure it matches, but we will just pass it back) + big_endian_32_t msg_type; ///< Message type (see rpc::msg_type) + big_endian_32_t rpc_version; ///< RPC protocol version (should be 2, but we can ignore) + big_endian_32_t program; ///< Program code (see rpc::programs) + big_endian_32_t program_version; ///< Program version - what version of the program is requested (we can ignore) + big_endian_32_t procedure; ///< Procedure code (see rpc::procedures) + big_endian_32_t credentials_l; ///< Security data (not used in this context) + big_endian_32_t credentials_h; ///< Security data (not used in this context) + big_endian_32_t verifier_l; ///< Security data (not used in this context) + big_endian_32_t verifier_h; ///< Security data (not used in this context) +}; + +/*! + @brief Structure of the minimum RPC response packet. + + All RPC/VXI responses will start with the data described + by this structure. Depending on the type of response, there + may be additional data as well. +*/ +struct rpc_response_packet { + big_endian_32_t xid; ///< Transaction id (we just pass it back what we received in the request) + big_endian_32_t msg_type; ///< Message type (see rpc::msg_type) + big_endian_32_t reply_state; ///< Accepted or rejected (see rpc::reply_state) + big_endian_32_t verifier_l; ///< Security data (not used in this context) + big_endian_32_t verifier_h; ///< Security data (not used in this context) + big_endian_32_t rpc_status; ///< Status of accepted message (see rpc::rpc_status) +}; + +/*! + @brief Structure of the RPC bind request packet. + + When the RPC packet is a bind request, it will include additional + data beyond the basic RPC/VXI request packet structure ... none of + which we will actually process. +*/ +struct bind_request_packet { + big_endian_32_t xid; ///< Transaction id (should be checked to make sure it matches, but we will just pass it back) + big_endian_32_t msg_type; ///< Message type (see rpc::msg_type) + big_endian_32_t rpc_version; ///< RPC protocol version (should be 2, but we can ignore) + big_endian_32_t program; ///< Program code (see rpc::programs) + big_endian_32_t program_version; ///< Program version - what version of the program is requested (we can ignore) + big_endian_32_t procedure; ///< Procedure code (see rpc::procedures) + big_endian_32_t credentials_l; ///< Security data (not used in this context) + big_endian_32_t credentials_h; ///< Security data (not used in this context) + big_endian_32_t verifier_l; ///< Security data (not used in this context) + big_endian_32_t verifier_h; ///< Security data (not used in this context) + big_endian_32_t getport_program; ///< We can ignore this + big_endian_32_t getport_version; ///< We can ignore this + big_endian_32_t getport_protocol; ///< We can ignore this + big_endian_32_t getport_port; ///< we can ignore this +}; + +/*! + @brief Structure of the RPC bind response packet. + + When a bind request is received, the response should consist of + the basic RPC/VXI request packet structure plus one more field + containing the port number on which the VXI_Server is currently + listening. +*/ +struct bind_response_packet { + big_endian_32_t xid; ///< Transaction id (we just pass it back what we received in the request) + big_endian_32_t msg_type; ///< Message type (see rpc::msg_type) + big_endian_32_t reply_state; ///< Accepted or rejected (see rpc::reply_state) + big_endian_32_t verifier_l; ///< Security data (not used in this context) + big_endian_32_t verifier_h; ///< Security data (not used in this context) + big_endian_32_t rpc_status; ///< Status of accepted message (see rpc::rpc_status) + big_endian_32_t vxi_port; ///< The port on which the VXI_Server is currently listening +}; + +/*! + @brief Structure of the VXI_11_CREATE_LINK request packet. + + In addition to the basic RPC request data, the CREATE_LINK request + includes a client id (optional), lock request, and instrument name. +*/ +struct create_request_packet { + big_endian_32_t xid; ///< Transaction id (should be checked to make sure it matches, but we will just pass it back) + big_endian_32_t msg_type; ///< Message type (see rpc::msg_type) + big_endian_32_t rpc_version; ///< RPC protocol version (should be 2, but we can ignore) + big_endian_32_t program; ///< Program code (see rpc::programs) + big_endian_32_t program_version; ///< Program version - what version of the program is requested (we can ignore) + big_endian_32_t procedure; ///< Procedure code (see rpc::procedures) + big_endian_32_t credentials_l; ///< Security data (not used in this context) + big_endian_32_t credentials_h; ///< Security data (not used in this context) + big_endian_32_t verifier_l; ///< Security data (not used in this context) + big_endian_32_t verifier_h; ///< Security data (not used in this context) + big_endian_32_t client_id; ///< implementation specific id (we can ignore) + big_endian_32_t lockDevice; ///< request to lock device; we will ignore this + big_endian_32_t lock_timeout; ///< time to wait for the lock; we will ignore this + big_endian_32_t data_len; ///< length of the string in data field + char data[]; ///< name of the instrument (e.g., instr0) +}; + +/*! + @brief Structure of the VXI_11_CREATE_LINK response packet. + + In addition to the basic RPC response data, the CREATE_LINK response + includes an error field, a link id to use throughout this link session, + a port # that can be used to issue an abort command, and the maximum length + of data that the instrument can receive per write request. +*/ +struct create_response_packet { + big_endian_32_t xid; ///< Transaction id (we just pass it back what we received in the request) + big_endian_32_t msg_type; ///< Message type (see rpc::msg_type) + big_endian_32_t reply_state; ///< Accepted or rejected (see rpc::reply_state) + big_endian_32_t verifier_l; ///< Security data (not used in this context) + big_endian_32_t verifier_h; ///< Security data (not used in this context) + big_endian_32_t rpc_status; ///< Status of accepted message (see rpc::rpc_status) + big_endian_32_t error; ///< Error code (see rpc::errors) + big_endian_32_t link_id; ///< A unique link id to be used by subsequent calls in this session + big_endian_32_t abort_port; ///< Port number on which the device will listen for an asynchronous abort request + big_endian_32_t max_receive_size; ///< maximum amount of data that can be received on each write command +}; + +/*! + @brief Structure of the VXI_11_DESTROY_LINK request packet. + + In addition to the basic RPC request data, the DESTROY_LINK request + includes the link id. +*/ +struct destroy_request_packet { + big_endian_32_t xid; ///< Transaction id (should be checked to make sure it matches, but we will just pass it back) + big_endian_32_t msg_type; ///< Message type (see rpc::msg_type) + big_endian_32_t rpc_version; ///< RPC protocol version (should be 2, but we can ignore) + big_endian_32_t program; ///< Program code (see rpc::programs) + big_endian_32_t program_version; ///< Program version - what version of the program is requested (we can ignore) + big_endian_32_t procedure; ///< Procedure code (see rpc::procedures) + big_endian_32_t credentials_l; ///< Security data (not used in this context) + big_endian_32_t credentials_h; ///< Security data (not used in this context) + big_endian_32_t verifier_l; ///< Security data (not used in this context) + big_endian_32_t verifier_h; ///< Security data (not used in this context) + big_endian_32_t link_id; ///< Unique link id generated for this session (see CREATE_LINK) +}; + +/*! + @brief Structure of the VXI_11_DESTROY_LINK response packet. + + In addition to the basic RPC response data, the DESTROY_LINK response + includes an error field (e.g., to signal an invalid link id). +*/ +struct destroy_response_packet { + big_endian_32_t xid; ///< Transaction id (we just pass it back what we received in the request) + big_endian_32_t msg_type; ///< Message type (see rpc::msg_type) + big_endian_32_t reply_state; ///< Accepted or rejected (see rpc::reply_state) + big_endian_32_t verifier_l; ///< Security data (not used in this context) + big_endian_32_t verifier_h; ///< Security data (not used in this context) + big_endian_32_t rpc_status; ///< Status of accepted message (see rpc::rpc_status) + big_endian_32_t error; ///< Error code (see rpc::errors) +}; + +/*! + @brief Structure of the VXI_11_DEV_READ request packet. + + In addition to the basic RPC request data, the DEV_READ request + includes the link id, request size (maximum amount of data to send + per response), timeouts for lock and i/o, flags (can signal the use + of a terminating character), and the terminating character if any. +*/ +struct read_request_packet { + big_endian_32_t xid; ///< Transaction id (should be checked to make sure it matches, but we will just pass it back) + big_endian_32_t msg_type; ///< Message type (see rpc::msg_type) + big_endian_32_t rpc_version; ///< RPC protocol version (should be 2, but we can ignore) + big_endian_32_t program; ///< Program code (see rpc::programs) + big_endian_32_t program_version; ///< Program version - what version of the program is requested (we can ignore) + big_endian_32_t procedure; ///< Procedure code (see rpc::procedures) + big_endian_32_t credentials_l; ///< Security data (not used in this context) + big_endian_32_t credentials_h; ///< Security data (not used in this context) + big_endian_32_t verifier_l; ///< Security data (not used in this context) + big_endian_32_t verifier_h; ///< Security data (not used in this context) + big_endian_32_t link_id; ///< Unique link id generated for this session (see CREATE_LINK) + big_endian_32_t request_size; ///< Maximum amount of data requested (we will assume that we never send more than can be received)) + big_endian_32_t io_timeout; ///< How long to wait before timing out the data request (we will ignore) + big_endian_32_t lock_timeout; ///< How long to wait before timing out a lock request (we will ignore) + big_endian_32_t flags; ///< Used to indicate whether an "end" character is supplied (we will ignore) + char term_char; ///< The "end" character (we will ignore) +}; + +/*! + @brief Structure of the VXI_11_DEV_READ response packet. + + In addition to the basic RPC response data, the DEV_READ response + includes an error field, the reason the data read ended, the + length of data that will be returned, and the data itself. +*/ +struct read_response_packet { + big_endian_32_t xid; ///< Transaction id (we just pass it back what we received in the request) + big_endian_32_t msg_type; ///< Message type (see rpc::msg_type) + big_endian_32_t reply_state; ///< Accepted or rejected (see rpc::reply_state) + big_endian_32_t verifier_l; ///< Security data (not used in this context) + big_endian_32_t verifier_h; ///< Security data (not used in this context) + big_endian_32_t rpc_status; ///< Status of accepted message (see rpc::rpc_status) + big_endian_32_t error; ///< Error code (see rpc::errors) + big_endian_32_t reason; ///< Indicates why the data read ended (see rpc::reasons) + big_endian_32_t data_len; ///< Length of the data returned + char data[]; ///< The data returned +}; + +/*! + @brief Structure of the VXI_11_DEV_WRITE request packet. + + In addition to the basic RPC request data, the DEV_WRITE request + includes the link id, timeouts for lock and i/o, flags (can signal + the addition of an "end" character at the end of the data), length + of the data being sent, and the data itself. +*/ +struct write_request_packet { + big_endian_32_t xid; ///< Transaction id (should be checked to make sure it matches, but we will just pass it back) + big_endian_32_t msg_type; ///< Message type (see rpc::msg_type) + big_endian_32_t rpc_version; ///< RPC protocol version (should be 2, but we can ignore) + big_endian_32_t program; ///< Program code (see rpc::programs) + big_endian_32_t program_version; ///< Program version - what version of the program is requested (we can ignore) + big_endian_32_t procedure; ///< Procedure code (see rpc::procedures) + big_endian_32_t credentials_l; ///< Security data (not used in this context) + big_endian_32_t credentials_h; ///< Security data (not used in this context) + big_endian_32_t verifier_l; ///< Security data (not used in this context) + big_endian_32_t verifier_h; ///< Security data (not used in this context) + big_endian_32_t link_id; ///< Unique link id generated for this session (see CREATE_LINK) + big_endian_32_t io_timeout; ///< How long to wait before timing out the data request (we will ignore) + big_endian_32_t lock_timeout; ///< How long to wait before timing out a lock request (we will ignore) + big_endian_32_t flags; ///< Used to indicate whether an "end" character is supplied (we will ignore) + big_endian_32_t data_len; ///< Length of the data sent + char data[]; ///< The data sent +}; + +/*! + @brief Structure of the VXI_11_DEV_WRITE response packet. + + In addition to the basic RPC response data, the DEV_WRITE response + includes an error field and the length of the data that was sent. +*/ +struct write_response_packet { + big_endian_32_t xid; ///< Transaction id (we just pass it back what we received in the request) + big_endian_32_t msg_type; ///< Message type (see rpc::msg_type) + big_endian_32_t reply_state; ///< Accepted or rejected (see rpc::reply_state) + big_endian_32_t verifier_l; ///< Security data (not used in this context) + big_endian_32_t verifier_h; ///< Security data (not used in this context) + big_endian_32_t rpc_status; ///< Status of accepted message (see rpc::rpc_status) + big_endian_32_t error; ///< Error code (see rpc::errors) + big_endian_32_t size; ///< Number of bytes sent +}; + +/* constant variables used to access the data buffers as the various structures defined above */ + +rpc_request_packet *const udp_request = (rpc_request_packet *)udp_request_packet_buffer; ///< udp_request accesses the udp_request_packet_buffer as a generic rpc request +rpc_response_packet *const udp_response = (rpc_response_packet *)udp_response_packet_buffer; ///< udp_response accesses the udp_response_packet_buffer as a generic rpc response + +bind_request_packet *const udp_bind_request = (bind_request_packet *)udp_request_packet_buffer; ///< udp_bind_request accesses the udp_request_packet_buffer as an rpc bind request +bind_response_packet *const udp_bind_response = (bind_response_packet *)udp_response_packet_buffer; ///< udp_bind_response accesses the udp_response_packet_buffer as an rpc bind response + +rpc_request_packet *const tcp_request = (rpc_request_packet *)tcp_request_packet_buffer; ///< tcp_request accesses the tcp_request_packet_buffer as a generic rpc request +rpc_response_packet *const tcp_response = (rpc_response_packet *)tcp_response_packet_buffer; ///< tcp_response accesses the tcp_response_packet_buffer as a generic rpc response + +tcp_prefix_packet *const tcp_request_prefix = (tcp_prefix_packet *)tcp_request_prefix_buffer; ///< tcp_request_prefix accesses the tcp_request_prefix_buffer as a tcp prefix +tcp_prefix_packet *const tcp_response_prefix = (tcp_prefix_packet *)tcp_response_prefix_buffer; ///< tcp_response_prefix accesses the tcp_response_prefix_buffer as a tcp prefix + +bind_request_packet *const tcp_bind_request = (bind_request_packet *)tcp_request_packet_buffer; ///< tcp_bind_request accesses the tcp_request_packet_buffer as an rpc bind request +bind_response_packet *const tcp_bind_response = (bind_response_packet *)tcp_response_packet_buffer; ///< tcp_bind_response accesses the tcp_response_packet_buffer as an rpc bind response + +rpc_request_packet *const vxi_request = (rpc_request_packet *)vxi_request_packet_buffer; ///< vxi_request accesses the vxi_request_packet_buffer as a generic rpc request +rpc_response_packet *const vxi_response = (rpc_response_packet *)vxi_response_packet_buffer; ///< vxi_response accesses the vxi_response_packet_buffer as a generic rpc response + +tcp_prefix_packet *const vxi_request_prefix = (tcp_prefix_packet *)vxi_request_prefix_buffer; ///< vxi_request_prefix accesses the vxi_request_prefix_buffer as a tcp prefix +tcp_prefix_packet *const vxi_response_prefix = (tcp_prefix_packet *)vxi_response_prefix_buffer; ///< vxi_response_prefix accesses the vxi_response_prefix_buffer as a tcp prefix + +create_request_packet *const create_request = (create_request_packet *)vxi_request_packet_buffer; ///< create_request accesses the vxi_request_packet_buffer as a create link request +create_response_packet *const create_response = (create_response_packet *)vxi_response_packet_buffer; ///< create_response accesses the vxi_response_packet_buffer as a create link response + +destroy_request_packet *const destroy_request = (destroy_request_packet *)vxi_request_packet_buffer; ///< destroy_request accesses the vxi_request_packet_buffer as a destroy link request +destroy_response_packet *const destroy_response = (destroy_response_packet *)vxi_response_packet_buffer; ///< destroy_response accesses the vxi_response_packet_buffer as a destroy link response + +read_request_packet *const read_request = (read_request_packet *)vxi_request_packet_buffer; ///< read_request accesses the vxi_request_packet_buffer as a read request +read_response_packet *const read_response = (read_response_packet *)vxi_response_packet_buffer; ///< read_response accesses the vxi_response_packet_buffer as a read response + +write_request_packet *const write_request = (write_request_packet *)vxi_request_packet_buffer; ///< write_request accesses the vxi_request_packet_buffer as a write request +write_response_packet *const write_response = (write_response_packet *)vxi_response_packet_buffer; ///< write_response accesses the vxi_response_packet_buffer as a write response diff --git a/src/vxi11_server/utilities.h b/src/vxi11_server/utilities.h new file mode 100644 index 0000000..831c1a7 --- /dev/null +++ b/src/vxi11_server/utilities.h @@ -0,0 +1,177 @@ +#pragma once +/*! + @file utilities.h + @brief Various helper functions: endian-ness conversions, logging +*/ + +#include // for logging +#include +#include + +/*! + @brief Manages storage and conversion of 4-byte big-endian data. + + The WiFi packets used by RPC_Server and VXI_Server transmit + data in big-endian format; however, the ESP8266 and C++ + use little-endian format. When a packet is read into a buffer + which has been described as a series of big_endian_32_t members, + this class allows automatic conversion from the big-endian + packet data to a little-endian C++ variable and vice-versa. +*/ +class big_endian_32_t +{ + private: + uint32_t b_e_data; ///< Data storage is treated as uint32_t, but the data in this member is actually big-endian + + public: + /*! + @brief Constructor takes a uint32_t and stores it in big-endian form. + + @param data little-endian data to be converted and stored as big-endian + */ + big_endian_32_t(uint32_t data) { b_e_data = htonl(data); } + + /*! + @brief Implicit or explicit conversion to little-endian uint32_t. + + @return Little-endian equivalent of big-endian data stored in m_data + */ + operator uint32_t() { return ntohl(b_e_data); } +}; + +/*! + @brief 4-byte integer that cycles through a defined range. + + The cyclic_uint32_t class allows storage, retrieval, + and increment/decrement of a value that must cycle + through a constrained range. When an increment or + decrement exceeds the limits of the range, the + value cycles to the opposite end of the range. +*/ +class cyclic_uint32_t +{ + private: + uint32_t m_data; + uint32_t m_start; + uint32_t m_end; + + public: + /*! + @brief The constructor requires the range start and end. + + Initial value may optionally be included as well; if it is + not included, the value will default to the start of the range. + */ + cyclic_uint32_t(uint32_t start, uint32_t end, uint32_t value = 0) + { + m_start = start < end ? start : end; + m_end = start < end ? end : start; + m_data = value >= m_start && value <= m_end ? value : m_start; + } + + /*! + @brief Cycle to the previous value. + + If the current value is at the start of the range, the + "previous value" will loop around to the end of the range. + Otherwise, the "previous value" is simply the current + value - 1. + + @return The previous value. + */ + uint32_t goto_prev() + { + m_data = m_data > m_start ? m_data - 1 : m_end; + return m_data; + } + + /*! + @brief Cycle to the next value. + + If the current value is at the end of the range, the + "next value" will loop around to the start of the range. + Otherwise, the "next value" is simply the current value + 1. + + @return The next value. + */ + uint32_t goto_next() + { + m_data = m_data < m_end ? m_data + 1 : m_start; + return m_data; + } + + /*! + @brief Pre-increment operator, e.g., ++port. + + @return The next value (the value after cyling forward). + */ + uint32_t operator++() + { + return goto_next(); + } + + /*! + @brief Post-increment operator, e.g., port++. + + @return The current value (the value before cyling forward). + */ + uint32_t operator++(int) + { + uint32_t temp = m_data; + goto_next(); + return temp; + } + + /*! + @brief Pre-decrement operator, e.g., --port. + + @return The previous value (the value after cyling backward). + */ + uint32_t operator--() + { + return goto_prev(); + } + + /*! + @brief Post-decrement operator, e.g., port--. + + @return The current value (the value before cyling backward). + */ + uint32_t operator--(int) // postfix version + { + uint32_t temp = m_data; + goto_prev(); + return temp; + } + + /*! + @brief Allows conversion to uint32_t. + + Example: + @code + regular_uint32_t = cyclic_uint32_t_instance(); + @endcode + + @return The current value. + */ + uint32_t operator()() + { + return m_data; + } + + /*! + @brief Implicit conversion or explicit cast to uint32_t. + + Example: + @code + regular_uint32_t = (uint32_t)cyclic_uint32_t_instance; + @endcode + + @return The current value. + */ + operator uint32_t() + { + return m_data; + } +}; + diff --git a/src/vxi11_server/vxi_server.cpp b/src/vxi11_server/vxi_server.cpp new file mode 100644 index 0000000..f2335f5 --- /dev/null +++ b/src/vxi11_server/vxi_server.cpp @@ -0,0 +1,210 @@ +#include "vxi_server.h" +#include "rpc_enums.h" +#include "rpc_packets.h" + +#include +#include + +VXI_Server::VXI_Server(SCPI_handler &scpi_handler) + : vxi_port(rpc::VXI_PORT_START, rpc::VXI_PORT_END), + scpi_handler(scpi_handler) +{ + /* We do not start the tcp_server port here, because + WiFi has likely not yet been initialized. Instead, + we wait until the begin() command. */ +} + +VXI_Server::~VXI_Server() +{ +} + +uint32_t VXI_Server::allocate() +{ + uint32_t port = 0; + + if (available()) { + port = vxi_port; // This is a cyclic counter, not a simple integer + } + return port; +} + +void VXI_Server::begin(bool bNext) +{ + if (bNext) { + client.stop(); + tcp_server.stop(); + + /* Note that vxi_port is not an ordinary uint32_t. It is + an instance of class cyclic_uint32_t, defined in utilities.h, + which is constrained to a range of values. The increment + operator will cause it to go to the next value, automatically + going back to the starting value once it exceeds the maximum + of its range. */ + + vxi_port++; + } + + tcp_server.begin(vxi_port); + + LOG_F("Listening for VXI commands on TCP port %u\n", (uint32_t)vxi_port); + if (rpc::VXI_PORT_START == rpc::VXI_PORT_END) { + if (MDNS.isRunning()) { + LOG_LN("VXI_Server advertising as vxi-11."); + auto scpi_service = MDNS.addService(NULL, "vxi-11", "tcp", (uint32_t)vxi_port); + MDNS.addServiceTxt(scpi_service, "version", SCPI_STD_VERSION_REVISION); + } + } + // else: no mDNS, port changes too often +} + +void VXI_Server::loop() +{ + if (client) // if a connection has been established on port + { + bool bClose = false; + int len = get_vxi_packet(client); + + if (len > 0) { + bClose = handle_packet(); + } + + if (bClose) { + LOG_F("Closing VXI connection on port %u\n", (uint32_t)vxi_port); + /* this method will stop the client and the tcp_server, then rotate + to the next port (within the specified range) and restart the + tcp_server to listen on that port. */ + begin_next(); + } + } else // i.e., if ! client + { + client = tcp_server.accept(); // see if a client is available (data has been sent on port) + + if (client) { + LOG_F("\nVXI connection established on port %u\n", (uint32_t)vxi_port); + } + } +} + +bool VXI_Server::handle_packet() +{ + bool bClose = false; + uint32_t rc = rpc::SUCCESS; + + if (vxi_request->program != rpc::VXI_11_CORE) { + rc = rpc::PROG_UNAVAIL; + + LOG_F("ERROR: Invalid program (expected VXI_11_CORE = 0x607AF; received 0x%08x)\n", (uint32_t)(vxi_request->program)); + + } else + switch (vxi_request->procedure) { + case rpc::VXI_11_CREATE_LINK: + create_link(); + break; + case rpc::VXI_11_DEV_READ: + read(); + break; + case rpc::VXI_11_DEV_WRITE: + write(); + break; + case rpc::VXI_11_DESTROY_LINK: + destroy_link(); + bClose = true; + break; + default: + LOG_F("Invalid VXI-11 procedure (received %u)\n", (uint32_t)(vxi_request->procedure)); + rc = rpc::PROC_UNAVAIL; + break; + } + + /* Response messages will be sent by the various routines above + when the program and procedure are recognized (and therefore + rc == rpc::SUCCESS). We only need to send a response here + if rc != rpc::SUCCESS. */ + + if (rc != rpc::SUCCESS) { + vxi_response->rpc_status = rc; + send_vxi_packet(client, sizeof(rpc_response_packet)); + } + + /* signal to caller whether the connection should be close (i.e., DESTROY_LINK) */ + + return bClose; +} + +void VXI_Server::create_link() +{ + /* The data field in a link request should contain a string + with the name of the requesting device. It may already + be null-terminated, but just in case, we will put in + the terminator. */ + + create_request->data[create_request->data_len] = 0; + LOG_F("CREATE LINK request from \"%s\" on port %u\n", create_request->data, (uint32_t)vxi_port); + /* Generate the response */ + create_response->rpc_status = rpc::SUCCESS; + create_response->error = rpc::NO_ERROR; + create_response->link_id = 0; + create_response->abort_port = 0; + create_response->max_receive_size = VXI_READ_SIZE - 4; + send_vxi_packet(client, sizeof(create_response_packet)); +} + +void VXI_Server::destroy_link() +{ + LOG_F("DESTROY LINK on port %u\n", (uint32_t)vxi_port); + destroy_response->rpc_status = rpc::SUCCESS; + destroy_response->error = rpc::NO_ERROR; + send_vxi_packet(client, sizeof(destroy_response_packet)); +} + +void VXI_Server::read() +{ + // This is where we read from the device + // FIXME: Fill this in + char readbuffer[] = "DUMMY"; + uint32_t len = strlen(readbuffer); + LOG_F("READ DATA on port %u; data sent = %s\n", (uint32_t)vxi_port, readbuffer); + read_response->rpc_status = rpc::SUCCESS; + read_response->error = rpc::NO_ERROR; + read_response->reason = rpc::END; + read_response->data_len = len; + strcpy(read_response->data, readbuffer); + + send_vxi_packet(client, sizeof(read_response_packet) + len); +} + +void VXI_Server::write() +{ + // This is where we write to the device + uint32_t wlen = write_request->data_len; + uint32_t len = wlen; + while (len > 0 && write_request->data[len - 1] == '\n') { + len--; + } + write_request->data[len] = 0; + LOG_F("WRITE DATA on port %u = \"%s\"\n", (uint32_t)vxi_port, write_request->data); + /* Parse and respond to the SCPI command */ + parse_scpi(write_request->data); // FIXME: Fill this in + /* Generate the response */ + write_response->rpc_status = rpc::SUCCESS; + write_response->error = rpc::NO_ERROR; + write_response->size = wlen; + send_vxi_packet(client, sizeof(write_response_packet)); +} + +/** + * @brief This method parses the SCPI commands and issues the appropriate commands to the device. + * + * @param buffer null terminated string to send to the device + */ +void VXI_Server::parse_scpi(char *buffer) +{ + +} + +const char *VXI_Server::get_visa_resource() +{ + static char visa_resource[40]; + sprintf(visa_resource, "TCPIP::%s::INSTR", WiFi.localIP().toString().c_str()); + return visa_resource; +} diff --git a/src/vxi11_server/vxi_server.h b/src/vxi11_server/vxi_server.h new file mode 100644 index 0000000..cd127b5 --- /dev/null +++ b/src/vxi11_server/vxi_server.h @@ -0,0 +1,58 @@ +#pragma once + +#include "utilities.h" +#include "wifi_ext.h" +#include + +/*! + @brief Interface with the rest of the device. +*/ +class SCPI_handler +{ + // TODO: fill in + public: + SCPI_handler() {}; + ~SCPI_handler() {}; +}; + +/*! + @brief Listens for and responds to VXI-11 requests. +*/ +class VXI_Server +{ + + public: + enum Read_Type { + rt_none = 0, + rt_identification = 1, + rt_parameters = 2 + }; + + public: + VXI_Server(SCPI_handler &scpi_handler); + ~VXI_Server(); + + void loop(); + void begin(bool bNext = false); + void begin_next() { begin(true); } + bool available() { return (!client); } + uint32_t allocate(); + uint32_t port() { return vxi_port; } + const char *get_visa_resource(); + + protected: + void create_link(); + void destroy_link(); + void read(); + void write(); + bool handle_packet(); + void parse_scpi(char *buffer); + + WiFiServer_ext tcp_server; + WiFiClient client; + Read_Type read_type; + uint32_t rw_channel; + cyclic_uint32_t vxi_port; + SCPI_handler &scpi_handler; +}; + diff --git a/src/vxi11_server/wifi_ext.h b/src/vxi11_server/wifi_ext.h new file mode 100644 index 0000000..fc4eb95 --- /dev/null +++ b/src/vxi11_server/wifi_ext.h @@ -0,0 +1,38 @@ +#pragma once + +/*! + @file wifi_ext.h + @brief Declaration and definition of WiFiServer_ext class. +*/ + +#include + +/*! + @brief Minor extension of WiFiServer class to allow initialization + without a port. + + As of 10/5/2024, the latest version of the ESP8266 board installation + (3.1.2) includes a version of WiFiServer that cannot be initialized + without a port. However,there is a newer version of the WiFiServer library + at https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266WiFi + which includes a default value for the port (23), allowing one to declare + a WiFiServer variable without specifying the port. We need the functionality + of this later version, but since it is not (yet) included by the esp8266 + board package, we must create this functionality using the WiFiServer_ext + class. Note that the port can be changed as needed via the begin() command. +*/ +class WiFiServer_ext : public WiFiServer { + + public: + + /*! + @brief Allow construction without parameters. + + The constructor defaults to port 23, the same + default port as newer versions of WiFiServer. + */ + WiFiServer_ext () + : WiFiServer(23) + {} +}; + From 084a41a2b77a14956efd24e01ba3cd95d1f03e52 Mon Sep 17 00:00:00 2001 From: hans boot Date: Sat, 22 Feb 2025 20:52:14 +0100 Subject: [PATCH 05/17] integrate vxi-11 in GUI, remove hislip --- include/riden_http_server/riden_http_server.h | 5 +- include/riden_scpi/riden_scpi.h | 4 -- scripts/test_pyvisa.py | 35 ++++++++++--- src/main.cpp | 4 +- src/riden_http_server/riden_http_server.cpp | 20 +++++--- src/riden_scpi/riden_scpi.cpp | 51 ++----------------- src/vxi11_server/vxi_server.cpp | 17 +++++++ src/vxi11_server/vxi_server.h | 3 ++ 8 files changed, 68 insertions(+), 71 deletions(-) diff --git a/include/riden_http_server/riden_http_server.h b/include/riden_http_server/riden_http_server.h index cbd3447..693a247 100644 --- a/include/riden_http_server/riden_http_server.h +++ b/include/riden_http_server/riden_http_server.h @@ -7,6 +7,7 @@ #include #include #include +#include #include @@ -18,7 +19,7 @@ namespace RidenDongle class RidenHttpServer { public: - explicit RidenHttpServer(RidenModbus &modbus, RidenScpi &scpi, RidenModbusBridge &bridge) : modbus(modbus), scpi(scpi), bridge(bridge), server(HTTP_RAW_PORT) {} + explicit RidenHttpServer(RidenModbus &modbus, RidenScpi &scpi, RidenModbusBridge &bridge, VXI_Server &vxi_server) : modbus(modbus), scpi(scpi), bridge(bridge), vxi_server(vxi_server), server(HTTP_RAW_PORT) {} bool begin(); void loop(void); uint16_t port(); @@ -27,6 +28,7 @@ class RidenHttpServer RidenModbus &modbus; RidenScpi &scpi; RidenModbusBridge &bridge; + VXI_Server &vxi_server; ESP8266WebServer server; void handle_root_get(); @@ -56,7 +58,6 @@ class RidenHttpServer const char *get_firmware_version(); const char *get_serial_number(); - const char *get_visa_resource(); }; } // namespace RidenDongle diff --git a/include/riden_scpi/riden_scpi.h b/include/riden_scpi/riden_scpi.h index b9a5c69..e25b644 100644 --- a/include/riden_scpi/riden_scpi.h +++ b/include/riden_scpi/riden_scpi.h @@ -13,11 +13,7 @@ #define WRITE_BUFFER_LENGTH (256) #define SCPI_INPUT_BUFFER_LENGTH 256 #define SCPI_ERROR_QUEUE_SIZE 17 -#if defined(USE_HISLIP) -#define DEFAULT_SCPI_PORT 4880 -#else #define DEFAULT_SCPI_PORT 5025 -#endif namespace RidenDongle { diff --git a/scripts/test_pyvisa.py b/scripts/test_pyvisa.py index 59182c7..91cbc84 100644 --- a/scripts/test_pyvisa.py +++ b/scripts/test_pyvisa.py @@ -6,12 +6,15 @@ formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("port", type=str, nargs='?', default=None, help="The port to use. Must be a Visa compatible connection string.") parser.add_argument("-n", action="store_true", default=False, help="No scan for test SCPI devices. Will be ignored when port is not defined.") + parser.add_argument("-t", action="store_true", default=False, help="Test repeated calls for 10 seconds. Will be ignored when port is not defined.") args = parser.parse_args() rm = pyvisa.ResourceManager() + repeat_tests = args.t skip_scan = args.n if not args.port: skip_scan = False + repeat_tests = False if not skip_scan: print("Scanning for VISA resources...") print("VISA Resources found: ", end='') @@ -38,11 +41,27 @@ inst.write_termination = "\n" print("Connected.") msgs = ["*IDN?"] - for m in msgs: - if m.endswith("?"): - print(f"Query \"{m}\" reply: ", end='') - r = inst.query(m).strip() - print(f"\"{r}\"") - else: - print(f"Write \"{m}\"") - inst.write(m) + if repeat_tests: + print("Repeating tests for 10 seconds...") + import time + start = time.time() + while time.time() - start < 10: + for m in msgs: + if m.endswith("?"): + print(f"Query \"{m}\" reply: ", end='') + r = inst.query(m).strip() + print(f"\"{r}\"") + else: + print(f"Write \"{m}\"") + inst.write(m) + else: + for m in msgs: + if m.endswith("?"): + print(f"Query \"{m}\" reply: ", end='') + r = inst.query(m).strip() + print(f"\"{r}\"") + else: + print(f"Write \"{m}\"") + inst.write(m) + inst.close() + print("Done.") diff --git a/src/main.cpp b/src/main.cpp index 6bdbaa0..1d68bb0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -41,10 +41,10 @@ static bool connected = false; static RidenModbus riden_modbus; static RidenScpi riden_scpi(riden_modbus); static RidenModbusBridge modbus_bridge(riden_modbus); -static RidenHttpServer http_server(riden_modbus, riden_scpi, modbus_bridge); static SCPI_handler scpi_handler; -static VXI_Server vxi_server(scpi_handler); ///< The VXI_Server +static VXI_Server vxi_server(scpi_handler); static RPC_Bind_Server rpc_bind_server(vxi_server); ///< The RPC_Bind_Server for the vxi server +static RidenHttpServer http_server(riden_modbus, riden_scpi, modbus_bridge, vxi_server); /** * Invoked by led_ticker to flash the LED. diff --git a/src/riden_http_server/riden_http_server.cpp b/src/riden_http_server/riden_http_server.cpp index 97189e4..769671f 100644 --- a/src/riden_http_server/riden_http_server.cpp +++ b/src/riden_http_server/riden_http_server.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -15,6 +16,7 @@ using namespace RidenDongle; static const String scpi_protocol = "SCPI"; static const String modbustcp_protocol = "Modbus TCP"; +static const String vxi11_protocol = "VXI-11"; static const std::list uart_baudrates = { 9600, 19200, @@ -360,6 +362,8 @@ void RidenHttpServer::handle_disconnect_client_post() scpi.disconnect_client(ip); } else if (protocol == modbustcp_protocol) { bridge.disconnect_client(ip); + } else if (protocol == vxi11_protocol) { + vxi_server.disconnect_client(ip); } } @@ -490,9 +494,10 @@ void RidenHttpServer::send_services() server.sendContent(" "); send_info_row("Web Server Port", String(this->port(), 10)); send_info_row("Modbus TCP Port", String(bridge.port(), 10)); - String scpi_port = String(scpi.port(), 10); - send_info_row("SCPI Port", scpi_port); - send_info_row("VISA Resource Address", get_visa_resource()); + send_info_row("VXI-11 Port", String(vxi_server.port(), 10)); + send_info_row("SCPI RAW Port", String(scpi.port(), 10)); + send_info_row("VISA Resource Address 1", vxi_server.get_visa_resource()); + send_info_row("VISA Resource Address 2", scpi.get_visa_resource()); server.sendContent(" "); server.sendContent(" "); server.sendContent(" "); @@ -509,6 +514,9 @@ void RidenHttpServer::send_connected_clients() server.sendContent(" "); server.sendContent(" "); server.sendContent(" "); + for (auto const &ip : vxi_server.get_connected_clients()) { + send_client_row(ip, vxi11_protocol); + } for (auto const &ip : scpi.get_connected_clients()) { send_client_row(ip, scpi_protocol); } @@ -590,7 +598,7 @@ void RidenHttpServer::handle_lxi_identification() subnet_mask.c_str(), mac_address.c_str(), gateway.c_str(), - get_visa_resource(), + scpi.get_visa_resource(), 0 // Guard against wrong parameters, such as ${9999} }; TinyTemplateEngineMemoryReader reader(LXI_IDENTIFICATION_TEMPLATE); @@ -626,7 +634,3 @@ const char *RidenHttpServer::get_serial_number() return serial_number_string; } -const char *RidenHttpServer::get_visa_resource() -{ - return scpi.get_visa_resource(); -} diff --git a/src/riden_scpi/riden_scpi.cpp b/src/riden_scpi/riden_scpi.cpp index f150380..2e42f2a 100644 --- a/src/riden_scpi/riden_scpi.cpp +++ b/src/riden_scpi/riden_scpi.cpp @@ -6,10 +6,6 @@ #include #include -#ifdef USE_HISLIP -//#include -#endif - #include #include #include @@ -18,7 +14,7 @@ // ************ // This file combines a socket server with an SCPI parser. // -// 3 different types of socket servers are supported or are potentially possible: +// 3 main different types of socket servers are supported or are potentially possible: // // ** RAW socket // The default. Requires no special flags. @@ -31,22 +27,22 @@ // if args.port.endswith("::SOCKET"): // inst.read_termination = "\n" // inst.write_termination = "\n" +// ==> this is in this file // // // ** VXI-11 -// Requires -D USE_VXI11 // widely supported // - Requires 2 socket services: portmap/rpcbind (port 111) and vxi-11 (any port you want) // - Discovery is done via portmap when replying to GETPORT VXI-11 Core. This should be on UDP and TCP, but you can get by in TCP. -// - Secondary discovery is done via mDNS, and the service name is "vxi-11" (_vxi-11._tcp). This however still requires the portmapper. +// - Secondary discovery is done via mDNS, and the service name is "vxi-11" (_vxi-11._tcp). That mapper points directly to the vxi server port. // - The VISA string is like: "TCPIP::::INSTR" // - The SCPI commands and responses are sent as binary data, with a header and a payload. // - VXI-11 has separate commands for reading and writing. // - is discoverable by pyvisa, and requires no special construction in Python +// ==> this is in a parallel server, see vxi11_server rpc_bind_server // // // ** HiSLIP -// Requires -D USE_HISLIP // see https://www.ivifoundation.org/downloads/Protocol%20Specifications/IVI-6.1_HiSLIP-2.0-2020-04-23.pdf // see https://lxistandard.org/members/Adopted%20Specifications/Latest%20Version%20of%20Standards_/LXI%20Version%201.6/LXI_HiSLIP_Extended_Function_1.3_2022-05-26.pdf // is a more modern protocol. It can use a synchronous and/or an asynchronous connection. @@ -724,15 +720,9 @@ bool RidenScpi::begin() tcpServer.setNoDelay(true); if (MDNS.isRunning()) { -#if defined(USE_HISLIP) - LOG_LN("RidenScpi advertising as hislip."); - auto scpi_service = MDNS.addService(NULL, "hislip", "tcp", tcpServer.port()); - MDNS.addServiceTxt(scpi_service, "version", SCPI_STD_VERSION_REVISION); -#else LOG_LN("RidenScpi advertising as scpi-raw."); auto scpi_service = MDNS.addService(NULL, "scpi-raw", "tcp", tcpServer.port()); MDNS.addServiceTxt(scpi_service, "version", SCPI_STD_VERSION_REVISION); -#endif } LOG_LN("RidenScpi initialized"); @@ -768,39 +758,10 @@ bool RidenScpi::loop() if (bytes_read > 0) { scpi_context.buffer.position += bytes_read; scpi_context.buffer.length += bytes_read; -#if defined(USE_HISLIP) -#error "HiSLIP is not yet supported, it requires 2 clients at the same time" - write_buffer_length = 0; - int rv = hs_process_data(scpi_context.buffer.data, scpi_context.buffer.length, write_buffer, &write_buffer_length, sizeof(write_buffer)); - - LOG_F("process return: %d, out buffer size: %u\n", rv, write_buffer_length); - switch(rv) - { - case 2: - // We have a complete message to be sent immediately - SCPI_FlushRaw(); - scpi_context.buffer.position = 0; - scpi_context.buffer.length = 0; - break; - case 1: - // TODO we have a complete payload - scpi_context.buffer.position = 0; - scpi_context.buffer.length = 0; - break; - case 0: - // we need more data - break; - default: - scpi_context.buffer.position = 0; - scpi_context.buffer.length = 0; - break; - } -#else uint8_t last_byte = scpi_context.buffer.data[scpi_context.buffer.position - 1]; if (last_byte == '\n') { SCPI_Input(&scpi_context, NULL, 0); } -#endif } } else { // Client is sending more data than we can handle @@ -849,11 +810,7 @@ void RidenScpi::reset_buffers() const char *RidenScpi::get_visa_resource() { static char visa_resource[40]; -#if defined(USE_HISLIP) - sprintf(visa_resource, "TCPIP::%s::hislip0,%u::INSTR", WiFi.localIP().toString().c_str(), port()); -#else sprintf(visa_resource, "TCPIP::%s::%u::SOCKET", WiFi.localIP().toString().c_str(), port()); -#endif return visa_resource; } diff --git a/src/vxi11_server/vxi_server.cpp b/src/vxi11_server/vxi_server.cpp index f2335f5..0f458fe 100644 --- a/src/vxi11_server/vxi_server.cpp +++ b/src/vxi11_server/vxi_server.cpp @@ -4,6 +4,7 @@ #include #include +#include VXI_Server::VXI_Server(SCPI_handler &scpi_handler) : vxi_port(rpc::VXI_PORT_START, rpc::VXI_PORT_END), @@ -208,3 +209,19 @@ const char *VXI_Server::get_visa_resource() sprintf(visa_resource, "TCPIP::%s::INSTR", WiFi.localIP().toString().c_str()); return visa_resource; } + +std::list VXI_Server::get_connected_clients() +{ + std::list connected_clients; + if (client && client.connected()) { + connected_clients.push_back(client.remoteIP()); + } + return connected_clients; +} + +void VXI_Server::disconnect_client(const IPAddress &ip) +{ + if (client && client.connected() && client.remoteIP() == ip) { + client.stop(); + } +} diff --git a/src/vxi11_server/vxi_server.h b/src/vxi11_server/vxi_server.h index cd127b5..ead5e01 100644 --- a/src/vxi11_server/vxi_server.h +++ b/src/vxi11_server/vxi_server.h @@ -3,6 +3,7 @@ #include "utilities.h" #include "wifi_ext.h" #include +#include /*! @brief Interface with the rest of the device. @@ -39,6 +40,8 @@ class VXI_Server uint32_t allocate(); uint32_t port() { return vxi_port; } const char *get_visa_resource(); + std::list get_connected_clients(); + void disconnect_client(const IPAddress &ip); protected: void create_link(); From c9a932ccb5b169b034982252d617b29ebf5bf230 Mon Sep 17 00:00:00 2001 From: hans boot Date: Sat, 22 Feb 2025 22:49:02 +0100 Subject: [PATCH 06/17] better readme --- README.md | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 25d1fa3..96db952 100644 --- a/README.md +++ b/README.md @@ -19,31 +19,32 @@ The firmware has been tested with various tools and libraries: - SCPI - [lxi-tools](https://github.com/lxi-tools/lxi-tools) - [EEZ Studio](https://www.envox.eu/studio/studio-introduction/) - + - [PyVISA](https://pyvisa.readthedocs.io/) ## Features - Modbus RTU client communicating with Riden power supply firmware. - Modbus TCP bridge. -- SCPI control. +- SCPI control + - via raw socket (VISA string: `TCPIP::::5025::SOCKET`) + - and via vxi-11 (VISA string: `TCPIP::::INSTR`). - Web interface to configure the dongle and update firmware. - Automatically set power supply clock based on NTP. - mDNS advertising. -- Handles approximately 65 queries/second using Modbus TCP or SCPI - (tested using Unisoft v1.41.1k, UART baudrate set at 921600). - +- Handles approximately 65 queries/second using Modbus TCP or raw socket SCPI + (tested using Unisoft v1.41.1k, UART baudrate set at 9600). ## Warning - When flashing the Riden WiFi module you _will_ erase the existing firmware. - The firmware provided in this repository comes with no warranty. - ## Query Performance The regular Riden power supply firmware is considerably slower than UniSoft, handling less than 10 queries/second. +Raw socket communication is also faster than VXI-11. ## Hardware Preparations @@ -61,14 +62,12 @@ three additional wires: GPIO0, EN, and 3.3V. In order to ease development you may want to terminate the wires in a Dupont header connector allowing you to more easily use an ESP01 USB Serial Adapter or similar. - ## Download the Firmware from GitHub Firmware files will be [released on GitHub](https://github.com/morgendagen/riden-dongle/releases) as part of the repository. - ## Compiling the Firmware You will need [PlatformIO](https://platformio.org/) in order to @@ -78,7 +77,6 @@ firmware. No configuration is necessary; simply execute `pio run` and wait. The firmware is located at `.pio/build/esp12e/firmware.bin`. - ## Flashing the Firmware Provided you have prepared the hardware as described, connect @@ -119,7 +117,6 @@ after a short while, and eventually stop. You should now be able to connect to the dongle at http://RDxxxx-ssssssss.local. - ## Using lxi-tools to Verify Installation Execute the command @@ -152,7 +149,6 @@ to set the voltage to 3.3V A description of the implemented commands is available in [SCPI_COMMANDS.md](SCPI_COMMANDS.md). - ## OTA firmware update In order to update the firmware, you may prefer @@ -163,7 +159,6 @@ it to a computer. From the `Configure` page you can upload a new firmware binary. - ## Limitations The Riden power supply firmware has some quirks as described @@ -195,7 +190,6 @@ the register matches the language set from the front panel. It is not possible to control keypad lock. - ## Credits - https://github.com/emelianov/modbus-esp8266 @@ -204,3 +198,4 @@ It is not possible to control keypad lock. - https://github.com/ShayBox/Riden - https://github.com/tzapu/WiFiManager - https://github.com/nayarsystems/posix_tz_db +- https://github.com/awakephd/espBode From dad808565cf69f682b22ca9c40f07db699b757ae Mon Sep 17 00:00:00 2001 From: hans boot Date: Sat, 22 Feb 2025 23:03:31 +0100 Subject: [PATCH 07/17] warning --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 96db952..9f226e7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# **Work in progress, not ready yet** + # Riden Dongle - A Multi-Protocol Firmware for the Riden WiFi Module This is an alternative firmware for the Riden WiFi module that From 64747b99c70a193415441fd4a19928e2879d9a69 Mon Sep 17 00:00:00 2001 From: hans boot Date: Sun, 23 Feb 2025 16:10:20 +0100 Subject: [PATCH 08/17] Communication tests performed --- README.md | 27 ++++++- platformio.ini | 1 - requirements.txt | 2 +- scripts/test_pyvisa.py | 117 ++++++++++++++++++++++--------- src/vxi11_server/rpc_packets.cpp | 2 + src/vxi11_server/utilities.h | 5 ++ src/vxi11_server/vxi_server.cpp | 7 +- 7 files changed, 123 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 9f226e7..92dfad3 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,32 @@ The firmware has been tested with various tools and libraries: The regular Riden power supply firmware is considerably slower than UniSoft, handling less than 10 queries/second. -Raw socket communication is also faster than VXI-11. +Raw socket SCPI communication is about 2ms per query faster than VXI-11. +On write operations, there is no speed difference between the 2 styles of communication. + +## VISA communication directives + +### VXI-11 + +Only the VXI-11 channel (`TCPIP::::INSTR`) will be auto discovered as of now. + +### Raw sockets + +When using the raw sockets (`TCPIP::::5025::SOCKET`), you must, like with most other raw socket devices, use + +```python + inst.read_termination = "\n" + inst.write_termination = "\n" +``` + +Also, when writing to the device (normally done with `.write(message)`), follow that operation with one of the following: + +- `.read()` (but then, you could probably have used `.query()` instead) +- sleep of more than 150ms + +... as your client device may put multiple commands in one network packet, leading to overflows, and silent lockup of the socket server in the dongle. You may need to reboot the dongle (can then still be done via the web interface) to restore operation. + +This type of problem does not exist with VXI-11. ## Hardware Preparations diff --git a/platformio.ini b/platformio.ini index c3e77bd..5be546d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -19,7 +19,6 @@ build_flags = -D DEFAULT_UART_BAUDRATE=9600 -D USE_FULL_ERROR_LIST -D MOCK_RIDEN - -D USE_VXI11 extra_scripts = pre:scripts/get_version.py diff --git a/requirements.txt b/requirements.txt index 743c865..35e4ebb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -platformio==6.1.13 +platformio>=6.1.13 diff --git a/scripts/test_pyvisa.py b/scripts/test_pyvisa.py index 91cbc84..37c64eb 100644 --- a/scripts/test_pyvisa.py +++ b/scripts/test_pyvisa.py @@ -1,20 +1,84 @@ import argparse import pyvisa +import time +import datetime + + +def test_query(inst, m: str): + start_query = datetime.datetime.now() + if m.endswith("?"): + print(f"Query \"{m}\" reply: ", end='') + try: + r = inst.query(m).strip() + except Exception as e: + print(f"\nError on query: {e}") + return False + print(f"\"{r}\"", end='') + else: + print(f"Write \"{m}\"", end='') + try: + inst.write(m) + # inst.flush(pyvisa.highlevel.constants.BufferOperation.flush_write_buffer) + except Exception as e: + print(f"\nError on write: {e}") + return False + delta_time = datetime.datetime.now() - start_query + print(f", taking {delta_time.total_seconds() * 1000:.1f} ms.") + if not m.endswith("?"): + time.sleep(0.15) + + return True + + +def test_device(port: str, repeat_query: int, timeout: int): + rm = pyvisa.ResourceManager() + start_connect = datetime.datetime.now() + print(f"Connecting to '{port}'", end='') + try: + inst = rm.open_resource(port, timeout=timeout) + except Exception as e: + print(f"\nError on connect: {e}") + return False + delta_time = datetime.datetime.now() - start_connect + print(f" succeeded, taking {delta_time.total_seconds() * 1000:.1f} ms.") + if port.endswith("::SOCKET"): + inst.read_termination = "\n" + inst.write_termination = "\n" + msgs = ["*IDN?"] + msgs = ["VOLT 1", "VOLT 2", "VOLT 3", "VOLT 4", "VOLT 5", "VOLT 6", "VOLT 7"] + if repeat_query > 0: + print(f"Repeating query tests for {repeat_query} seconds...") + start = time.time() + while time.time() - start < repeat_query: + for m in msgs: + if not test_query(inst, m): + inst.close() + return False + else: + for m in msgs: + if not test_query(inst, m): + inst.close() + return False + return True + if __name__ == '__main__': parser = argparse.ArgumentParser(description="Test simple SCPI communication via VISA.", formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("port", type=str, nargs='?', default=None, help="The port to use. Must be a Visa compatible connection string.") - parser.add_argument("-n", action="store_true", default=False, help="No scan for test SCPI devices. Will be ignored when port is not defined.") - parser.add_argument("-t", action="store_true", default=False, help="Test repeated calls for 10 seconds. Will be ignored when port is not defined.") + parser.add_argument("port", type=str, nargs='?', default=None, help="The port to use for tests. Must be a Visa compatible connection string.") + parser.add_argument("-n", action="store_true", default=False, help="No SCPI device discovery preceding the test. Will be ignored when port is not defined.") + parser.add_argument("-c", type=int, default=0, help="Test repeated connect calls for N seconds. If not specified: will do 1 connect. Will be ignored when port is not defined.") + parser.add_argument("-q", type=int, default=0, help="After a connect, test repeated query calls for N seconds. If not specified: will do 1 call. Will be ignored when port is not defined.") + parser.add_argument("-t", type=int, default=10000, help="Timeout for any VISA operation in milliseconds for the repeated tests (-q, -c).") args = parser.parse_args() rm = pyvisa.ResourceManager() - repeat_tests = args.t + repeat_query = args.q + repeat_connect = args.c + test_timeout = args.t skip_scan = args.n if not args.port: skip_scan = False - repeat_tests = False if not skip_scan: print("Scanning for VISA resources...") print("VISA Resources found: ", end='') @@ -25,43 +89,28 @@ # some of these devices are not SCPI compatible, like "ASRL/dev/cu.Bluetooth-Incoming-Port::INSTR" print(f"Skipping serial port \"{m}\"") continue + inst = None + try: + inst = rm.open_resource(m, timeout=test_timeout) + except: + print(f"Cannot connect to device on address \"{m}\"") + continue try: - inst = rm.open_resource(m, timeout=1000) r = inst.query("*IDN?").strip() + inst.close() print(f"Found \"{r}\" on address \"{m}\"") except: print(f"Found unknown device on address \"{m}\"") + inst.close() else: print("No scan for VISA resources.") if args.port: - print(f"Connecting to '{args.port}'") - inst = rm.open_resource(args.port, timeout=2000) - if args.port.endswith("::SOCKET"): - inst.read_termination = "\n" - inst.write_termination = "\n" - print("Connected.") - msgs = ["*IDN?"] - if repeat_tests: - print("Repeating tests for 10 seconds...") - import time + if repeat_connect: + print(f"Repeating tests for {repeat_connect} seconds...") start = time.time() - while time.time() - start < 10: - for m in msgs: - if m.endswith("?"): - print(f"Query \"{m}\" reply: ", end='') - r = inst.query(m).strip() - print(f"\"{r}\"") - else: - print(f"Write \"{m}\"") - inst.write(m) + while time.time() - start < repeat_connect: + test_device(args.port, repeat_query, test_timeout) else: - for m in msgs: - if m.endswith("?"): - print(f"Query \"{m}\" reply: ", end='') - r = inst.query(m).strip() - print(f"\"{r}\"") - else: - print(f"Write \"{m}\"") - inst.write(m) - inst.close() + test_device(args.port, repeat_query, test_timeout) + print("Done.") diff --git a/src/vxi11_server/rpc_packets.cpp b/src/vxi11_server/rpc_packets.cpp index 06dec64..0ce5f5e 100644 --- a/src/vxi11_server/rpc_packets.cpp +++ b/src/vxi11_server/rpc_packets.cpp @@ -149,6 +149,7 @@ void send_bind_packet(WiFiClient &tcp, uint32_t len) ; // wait for tcp to be available tcp.write(tcp_response_prefix_buffer, len + 4); // add 4 to the length to account for the tcp_response_prefix + tcp.flush(); LOG_F("\nSent %d bytes to %s:%d\n", len, tcp.remoteIP().toString().c_str(), tcp.remotePort()); LOG_DUMP(tcp_response_prefix_buffer, len + 4) @@ -183,6 +184,7 @@ void send_vxi_packet(WiFiClient &tcp, uint32_t len) ; // wait for tcp to be available tcp.write(vxi_response_prefix_buffer, len + 4); // add 4 to the length to account for the vxi_response_prefix + tcp.flush(); LOG_F("\nSent %d bytes to %s:%d\n", len, tcp.remoteIP().toString().c_str(), tcp.remotePort()); LOG_DUMP(vxi_response_prefix_buffer, len + 4) diff --git a/src/vxi11_server/utilities.h b/src/vxi11_server/utilities.h index 831c1a7..23189f8 100644 --- a/src/vxi11_server/utilities.h +++ b/src/vxi11_server/utilities.h @@ -69,6 +69,11 @@ class cyclic_uint32_t m_data = value >= m_start && value <= m_end ? value : m_start; } + /*! + @return True if the counter is not cyclic in reality. + */ + bool is_noncyclic() { return m_start == m_end; } + /*! @brief Cycle to the previous value. diff --git a/src/vxi11_server/vxi_server.cpp b/src/vxi11_server/vxi_server.cpp index 0f458fe..5d73c79 100644 --- a/src/vxi11_server/vxi_server.cpp +++ b/src/vxi11_server/vxi_server.cpp @@ -33,6 +33,11 @@ void VXI_Server::begin(bool bNext) { if (bNext) { client.stop(); + + if (vxi_port.is_noncyclic()) return; // no need to change port, and the rest is already done + + // counter is cyclic, so we need to stop the server and rotate to the next port + LOG_F("Stop Listening for VXI commands on TCP port %u\n", (uint32_t)vxi_port); tcp_server.stop(); /* Note that vxi_port is not an ordinary uint32_t. It is @@ -48,7 +53,7 @@ void VXI_Server::begin(bool bNext) tcp_server.begin(vxi_port); LOG_F("Listening for VXI commands on TCP port %u\n", (uint32_t)vxi_port); - if (rpc::VXI_PORT_START == rpc::VXI_PORT_END) { + if (vxi_port.is_noncyclic()) { if (MDNS.isRunning()) { LOG_LN("VXI_Server advertising as vxi-11."); auto scpi_service = MDNS.addService(NULL, "vxi-11", "tcp", (uint32_t)vxi_port); From 113eea91bc7cb951621e10d5c8e43f380f28a08c Mon Sep 17 00:00:00 2001 From: hans boot Date: Sun, 23 Feb 2025 17:34:15 +0100 Subject: [PATCH 09/17] repair buffer overflow in socket handler --- README.md | 13 +++++-------- scripts/test_pyvisa.py | 12 +++++++----- src/riden_scpi/riden_scpi.cpp | 35 +++++++++++++++++++++-------------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 92dfad3..a8b147d 100644 --- a/README.md +++ b/README.md @@ -53,10 +53,12 @@ On write operations, there is no speed difference between the 2 styles of commun ### VXI-11 -Only the VXI-11 channel (`TCPIP::::INSTR`) will be auto discovered as of now. +The VXI-11 channel (`TCPIP::::INSTR`) is auto discoverable via mDNS, TCP and UDP, making it highly compatible with most tools. ### Raw sockets +Raw socket capability cannot be auto discovered by pyvisa as of now. It can be discovered by lxi tools (see below) + When using the raw sockets (`TCPIP::::5025::SOCKET`), you must, like with most other raw socket devices, use ```python @@ -64,14 +66,9 @@ When using the raw sockets (`TCPIP::::5025::SOCKET`), you must, like inst.write_termination = "\n" ``` -Also, when writing to the device (normally done with `.write(message)`), follow that operation with one of the following: - -- `.read()` (but then, you could probably have used `.query()` instead) -- sleep of more than 150ms - -... as your client device may put multiple commands in one network packet, leading to overflows, and silent lockup of the socket server in the dongle. You may need to reboot the dongle (can then still be done via the web interface) to restore operation. +Also, be aware that when writing many commands to the device, the network and the device will queue them up. As a result, the delay between the moment your client issues a command, and the moment the device handles the command, can be significant. If you do not want that, insert a sleep of more than 150ms after each write command, forcing the network to send 1 command at a time. -This type of problem does not exist with VXI-11. +VXI-11 does not have this problem, since every command requires an ACK. ## Hardware Preparations diff --git a/scripts/test_pyvisa.py b/scripts/test_pyvisa.py index 37c64eb..672f8c1 100644 --- a/scripts/test_pyvisa.py +++ b/scripts/test_pyvisa.py @@ -4,7 +4,7 @@ import datetime -def test_query(inst, m: str): +def test_query(inst, m: str, write_delay_ms: int = 0): start_query = datetime.datetime.now() if m.endswith("?"): print(f"Query \"{m}\" reply: ", end='') @@ -24,8 +24,8 @@ def test_query(inst, m: str): return False delta_time = datetime.datetime.now() - start_query print(f", taking {delta_time.total_seconds() * 1000:.1f} ms.") - if not m.endswith("?"): - time.sleep(0.15) + if not m.endswith("?") and write_delay_ms > 0: + time.sleep(write_delay_ms / 1000) return True @@ -41,9 +41,11 @@ def test_device(port: str, repeat_query: int, timeout: int): return False delta_time = datetime.datetime.now() - start_connect print(f" succeeded, taking {delta_time.total_seconds() * 1000:.1f} ms.") + write_delay_ms = 0 if port.endswith("::SOCKET"): inst.read_termination = "\n" inst.write_termination = "\n" + # write_delay_ms = 150 # for socket connections, a delay is needed between writes sometimes msgs = ["*IDN?"] msgs = ["VOLT 1", "VOLT 2", "VOLT 3", "VOLT 4", "VOLT 5", "VOLT 6", "VOLT 7"] if repeat_query > 0: @@ -51,12 +53,12 @@ def test_device(port: str, repeat_query: int, timeout: int): start = time.time() while time.time() - start < repeat_query: for m in msgs: - if not test_query(inst, m): + if not test_query(inst, m, write_delay_ms): inst.close() return False else: for m in msgs: - if not test_query(inst, m): + if not test_query(inst, m, write_delay_ms): inst.close() return False return True diff --git a/src/riden_scpi/riden_scpi.cpp b/src/riden_scpi/riden_scpi.cpp index 2e42f2a..59cba9c 100644 --- a/src/riden_scpi/riden_scpi.cpp +++ b/src/riden_scpi/riden_scpi.cpp @@ -179,6 +179,7 @@ size_t RidenScpi::SCPI_Write(scpi_t *context, const char *data, size_t len) scpi_result_t RidenScpi::SCPI_Flush(scpi_t *context) { RidenScpi *ridenScpi = static_cast(context->user_context); + LOG_F("SCPI_Flush: sending \"%.*s\"\n", ridenScpi->write_buffer_length, ridenScpi->write_buffer); return ridenScpi->SCPI_FlushRaw(); } @@ -449,6 +450,8 @@ scpi_result_t RidenScpi::SourceVoltage(scpi_t *context) scpi_choice_def_t special; scpi_number_t value; + LOG_F("SourceVoltage command\n"); + if (!SCPI_ParamNumber(context, &special, &value, TRUE)) { return SCPI_RES_ERR; } @@ -733,7 +736,6 @@ bool RidenScpi::begin() bool RidenScpi::loop() { - // TODO This does not work with hislip, where we need 2 connections at the same time // Check for new client connecting WiFiClient newClient = tcpServer.accept(); if (newClient) { @@ -752,20 +754,25 @@ bool RidenScpi::loop() if (client) { int bytes_available = client.available(); if (bytes_available > 0) { - int space_left = SCPI_INPUT_BUFFER_LENGTH - scpi_context.buffer.position; - if (space_left >= bytes_available) { - int bytes_read = client.readBytes(&(scpi_context.buffer.data[scpi_context.buffer.position]), bytes_available); - if (bytes_read > 0) { - scpi_context.buffer.position += bytes_read; - scpi_context.buffer.length += bytes_read; - uint8_t last_byte = scpi_context.buffer.data[scpi_context.buffer.position - 1]; - if (last_byte == '\n') { - SCPI_Input(&scpi_context, NULL, 0); - } + // Now read until I find a newline. There may be way more data in the buffer than 1 command. + char buffer[1]; + while (client.readBytes(buffer, 1) == 1) { + if (scpi_context.buffer.position >= SCPI_INPUT_BUFFER_LENGTH) { + // Client is sending more data than we can handle + LOG_F("ERROR: RidenScpi buffer overflow. Flushing data and killing connection.\n"); + scpi_context.buffer.position = 0; + scpi_context.buffer.length = 0; + client.stop(); + break; + } + scpi_context.buffer.data[scpi_context.buffer.position] = buffer[0]; + scpi_context.buffer.position++; + scpi_context.buffer.length++; + if (buffer[0] == '\n') { + LOG_F("RidenScpi: received %d bytes for handling\n", scpi_context.buffer.position); + SCPI_Input(&scpi_context, NULL, 0); + break; } - } else { - // Client is sending more data than we can handle - client.stop(); } } } From 97d031bf993bae3c7ed949262e4def4dcd021267 Mon Sep 17 00:00:00 2001 From: hans boot Date: Sun, 23 Feb 2025 20:06:08 +0100 Subject: [PATCH 10/17] functional and robust vxi server. only error handling lacks --- include/riden_scpi/riden_scpi.h | 13 ++++- platformio.ini | 2 +- scripts/test_pyvisa.py | 2 +- src/main.cpp | 15 +++--- src/riden_scpi/riden_scpi.cpp | 87 +++++++++++++++++++++++++++------ src/scpi_bridge/scpi_bridge.h | 32 ++++++++++++ src/vxi11_server/vxi_server.cpp | 39 +++++++-------- src/vxi11_server/vxi_server.h | 15 +++--- 8 files changed, 154 insertions(+), 51 deletions(-) create mode 100644 src/scpi_bridge/scpi_bridge.h diff --git a/include/riden_scpi/riden_scpi.h b/include/riden_scpi/riden_scpi.h index e25b644..2936c9c 100644 --- a/include/riden_scpi/riden_scpi.h +++ b/include/riden_scpi/riden_scpi.h @@ -29,9 +29,15 @@ class RidenScpi uint16_t port(); std::list get_connected_clients(); void disconnect_client(const IPAddress &ip); - const char *get_visa_resource(); + // some inferface functions to handle commands to the SCPI parser from an outside source + // TODO: The SCPI parser should be externalised into another class and instance + void claim_external_control() { external_control = true; } + void release_external_control() { external_control = false; } + void write(const char *data, size_t len); + scpi_result_t read(char *data, size_t *len, size_t max_len); + private: RidenModbus &ridenModbus; @@ -48,6 +54,11 @@ class RidenScpi char write_buffer[WRITE_BUFFER_LENGTH] = {}; size_t write_buffer_length = 0; + // external_control is used to indicate that the SCPI parser is handling a command from outside of the socket server + // See claim_external_control() and release_external_control() + bool external_control = false; + bool external_output_ready = false; + static const scpi_command_t scpi_commands[]; static scpi_interface_t scpi_interface; diff --git a/platformio.ini b/platformio.ini index 5be546d..6043602 100644 --- a/platformio.ini +++ b/platformio.ini @@ -18,7 +18,7 @@ lib_deps = build_flags = -D DEFAULT_UART_BAUDRATE=9600 -D USE_FULL_ERROR_LIST - -D MOCK_RIDEN +# -D MOCK_RIDEN extra_scripts = pre:scripts/get_version.py diff --git a/scripts/test_pyvisa.py b/scripts/test_pyvisa.py index 672f8c1..d001e31 100644 --- a/scripts/test_pyvisa.py +++ b/scripts/test_pyvisa.py @@ -47,7 +47,7 @@ def test_device(port: str, repeat_query: int, timeout: int): inst.write_termination = "\n" # write_delay_ms = 150 # for socket connections, a delay is needed between writes sometimes msgs = ["*IDN?"] - msgs = ["VOLT 1", "VOLT 2", "VOLT 3", "VOLT 4", "VOLT 5", "VOLT 6", "VOLT 7"] + msgs = ["VOLT 1", "VOLT?", "VOLT 2", "VOLT?", "VOLT 3", "VOLT?"] if repeat_query > 0: print(f"Repeating query tests for {repeat_query} seconds...") start = time.time() diff --git a/src/main.cpp b/src/main.cpp index 1d68bb0..533ab7b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -38,13 +39,13 @@ static bool did_update_time = false; static bool connected = false; -static RidenModbus riden_modbus; -static RidenScpi riden_scpi(riden_modbus); -static RidenModbusBridge modbus_bridge(riden_modbus); -static SCPI_handler scpi_handler; -static VXI_Server vxi_server(scpi_handler); -static RPC_Bind_Server rpc_bind_server(vxi_server); ///< The RPC_Bind_Server for the vxi server -static RidenHttpServer http_server(riden_modbus, riden_scpi, modbus_bridge, vxi_server); +static RidenModbus riden_modbus; ///< The modbus server +static RidenScpi riden_scpi(riden_modbus); ///< The raw socket server + the SCPI command handler +static RidenModbusBridge modbus_bridge(riden_modbus); ///< The modbus TCP server +static SCPI_handler scpi_handler(riden_scpi); ///< The bridge from the vxi server to the SCPI command handler +static VXI_Server vxi_server(scpi_handler); ///< The vxi server +static RPC_Bind_Server rpc_bind_server(vxi_server); ///< The RPC_Bind_Server for the vxi server +static RidenHttpServer http_server(riden_modbus, riden_scpi, modbus_bridge, vxi_server); ///< The web server /** * Invoked by led_ticker to flash the LED. diff --git a/src/riden_scpi/riden_scpi.cpp b/src/riden_scpi/riden_scpi.cpp index 59cba9c..c3d8a18 100644 --- a/src/riden_scpi/riden_scpi.cpp +++ b/src/riden_scpi/riden_scpi.cpp @@ -169,28 +169,29 @@ size_t SCPI_ResultChoice(scpi_t *context, scpi_choice_def_t *options, int32_t va size_t RidenScpi::SCPI_Write(scpi_t *context, const char *data, size_t len) { RidenScpi *ridenScpi = static_cast(context->user_context); - + LOG_F("SCPI_Write: writing \"%.*s\"\n", (int)len, data); + ridenScpi->external_output_ready = false; // don't send half baked data to the client memcpy(&(ridenScpi->write_buffer[ridenScpi->write_buffer_length]), data, len); ridenScpi->write_buffer_length += len; - + return len; } scpi_result_t RidenScpi::SCPI_Flush(scpi_t *context) { RidenScpi *ridenScpi = static_cast(context->user_context); - LOG_F("SCPI_Flush: sending \"%.*s\"\n", ridenScpi->write_buffer_length, ridenScpi->write_buffer); - return ridenScpi->SCPI_FlushRaw(); -} -scpi_result_t RidenScpi::SCPI_FlushRaw(void) -{ - if (client) { - client.write(write_buffer, write_buffer_length); - write_buffer_length = 0; - client.flush(); + if (ridenScpi->external_control) { + // do not write to the client, let the read function fetch the data + ridenScpi->external_output_ready = true; + return SCPI_RES_OK; + } + LOG_F("SCPI_Flush: sending \"%.*s\"\n", (int)ridenScpi->write_buffer_length, ridenScpi->write_buffer); + if (ridenScpi->client) { + ridenScpi->client.write(ridenScpi->write_buffer, ridenScpi->write_buffer_length); + ridenScpi->write_buffer_length = 0; + ridenScpi->client.flush(); } - return SCPI_RES_OK; } @@ -691,9 +692,57 @@ scpi_result_t RidenScpi::SystemBeeperStateQ(scpi_t *context) } } +/** + * @brief Write data to the parser and the device. + * It overwrites the data in the buffer from the raw socket server. + * + * @param data data to be sent + * @param len length of data + */ +void RidenScpi::write(const char *data, size_t len) +{ + if ((len == 0) || (data == NULL)) return; + // insert the data into the buffer. + if (len > SCPI_INPUT_BUFFER_LENGTH) { + LOG_F("ERROR: RidenScpi buffer overflow. Ignoring data.\n"); + return; + } + memcpy(scpi_context.buffer.data, data, len); + scpi_context.buffer.position = len; + scpi_context.buffer.length = len; + external_control = true; // just to be sure + SCPI_Input(&scpi_context, NULL, 0); +} + +/** + * @brief Read data from the parser and the device, this is the reaction to "write()" + * + * @param data buffer to copy the data into + * @param len length of data + * @param max_len maximum length of data + * @return scpi_result_t last error code + */ +scpi_result_t RidenScpi::read(char *data, size_t *len, size_t max_len){ + if (!external_control || len == NULL || data == NULL) { + return SCPI_RES_ERR; + } + *len = 0; + if (write_buffer_length > max_len) { + LOG_F("ERROR: RidenScpi output buffer overflow. Flushing the data.\n"); + return SCPI_RES_ERR; + } + if (!external_output_ready) { + + return SCPI_RES_ERR; + } + memcpy(data, write_buffer, write_buffer_length); + *len = write_buffer_length; + write_buffer_length = 0; + return SCPI_RES_OK; +} + bool RidenScpi::begin() { - // TODO adapt to hislip if (initialized) { return true; } @@ -736,6 +785,15 @@ bool RidenScpi::begin() bool RidenScpi::loop() { + if (external_control) { + // skip this loop if I'm under external control + if (client) { + LOG_LN("RidenScpi: disconnect client because I am under external control."); + client.stop(); + } + return true; + } + // Check for new client connecting WiFiClient newClient = tcpServer.accept(); if (newClient) { @@ -764,7 +822,8 @@ bool RidenScpi::loop() scpi_context.buffer.length = 0; client.stop(); break; - } + } + // insert the character into the buffer. scpi_context.buffer.data[scpi_context.buffer.position] = buffer[0]; scpi_context.buffer.position++; scpi_context.buffer.length++; diff --git a/src/scpi_bridge/scpi_bridge.h b/src/scpi_bridge/scpi_bridge.h new file mode 100644 index 0000000..958c0b9 --- /dev/null +++ b/src/scpi_bridge/scpi_bridge.h @@ -0,0 +1,32 @@ +#include +#include +#include +#include + +class SCPI_handler: public SCPI_handler_interface +{ + public: + SCPI_handler(RidenDongle::RidenScpi &ridenScpi): ridenScpi(ridenScpi) {} + + void write(const char *data, size_t len) override + { + ridenScpi.write(data, len); + } + scpi_result_t read(char *data, size_t *len, size_t max_len) override + { + return ridenScpi.read(data, len, max_len); + } + void claim_control() override + { + ridenScpi.claim_external_control(); + } + void release_control() override + { + ridenScpi.release_external_control(); + } + + private: + RidenDongle::RidenScpi &ridenScpi; +}; + + diff --git a/src/vxi11_server/vxi_server.cpp b/src/vxi11_server/vxi_server.cpp index 5d73c79..aa64214 100644 --- a/src/vxi11_server/vxi_server.cpp +++ b/src/vxi11_server/vxi_server.cpp @@ -6,7 +6,7 @@ #include #include -VXI_Server::VXI_Server(SCPI_handler &scpi_handler) +VXI_Server::VXI_Server(SCPI_handler_interface &scpi_handler) : vxi_port(rpc::VXI_PORT_START, rpc::VXI_PORT_END), scpi_handler(scpi_handler) { @@ -153,6 +153,7 @@ void VXI_Server::create_link() create_response->abort_port = 0; create_response->max_receive_size = VXI_READ_SIZE - 4; send_vxi_packet(client, sizeof(create_response_packet)); + scpi_handler.claim_control(); } void VXI_Server::destroy_link() @@ -161,20 +162,24 @@ void VXI_Server::destroy_link() destroy_response->rpc_status = rpc::SUCCESS; destroy_response->error = rpc::NO_ERROR; send_vxi_packet(client, sizeof(destroy_response_packet)); + scpi_handler.release_control(); } void VXI_Server::read() { // This is where we read from the device - // FIXME: Fill this in - char readbuffer[] = "DUMMY"; - uint32_t len = strlen(readbuffer); - LOG_F("READ DATA on port %u; data sent = %s\n", (uint32_t)vxi_port, readbuffer); + char outbuffer[256]; + size_t len = 0; + scpi_result_t rv = scpi_handler.read(outbuffer, &len, sizeof(outbuffer)); + + // FIXME handle error codes, maybe even pick up errors from the SCPI Parser + + LOG_F("READ DATA on port %u; data sent = %.*s\n", (uint32_t)vxi_port, (int)len, outbuffer); read_response->rpc_status = rpc::SUCCESS; read_response->error = rpc::NO_ERROR; read_response->reason = rpc::END; - read_response->data_len = len; - strcpy(read_response->data, readbuffer); + read_response->data_len = (uint32_t)len; + strcpy(read_response->data, outbuffer); send_vxi_packet(client, sizeof(read_response_packet) + len); } @@ -184,30 +189,22 @@ void VXI_Server::write() // This is where we write to the device uint32_t wlen = write_request->data_len; uint32_t len = wlen; - while (len > 0 && write_request->data[len - 1] == '\n') { + // right trim. SCPI parser doesn't like \r\n + while (len > 0 && isspace(write_request->data[len - 1])) { len--; } write_request->data[len] = 0; - LOG_F("WRITE DATA on port %u = \"%s\"\n", (uint32_t)vxi_port, write_request->data); + LOG_F("WRITE DATA on port %u = \"%.*s\"\n", (uint32_t)vxi_port, (int)len, write_request->data); /* Parse and respond to the SCPI command */ - parse_scpi(write_request->data); // FIXME: Fill this in + scpi_handler.write(write_request->data, len); + /* Generate the response */ write_response->rpc_status = rpc::SUCCESS; write_response->error = rpc::NO_ERROR; - write_response->size = wlen; + write_response->size = wlen; // with the original length send_vxi_packet(client, sizeof(write_response_packet)); } -/** - * @brief This method parses the SCPI commands and issues the appropriate commands to the device. - * - * @param buffer null terminated string to send to the device - */ -void VXI_Server::parse_scpi(char *buffer) -{ - -} - const char *VXI_Server::get_visa_resource() { static char visa_resource[40]; diff --git a/src/vxi11_server/vxi_server.h b/src/vxi11_server/vxi_server.h index ead5e01..ce1a19b 100644 --- a/src/vxi11_server/vxi_server.h +++ b/src/vxi11_server/vxi_server.h @@ -3,17 +3,20 @@ #include "utilities.h" #include "wifi_ext.h" #include +#include #include /*! @brief Interface with the rest of the device. */ -class SCPI_handler +class SCPI_handler_interface { - // TODO: fill in public: - SCPI_handler() {}; - ~SCPI_handler() {}; + virtual ~SCPI_handler_interface() {} + virtual void write(const char *data, size_t len) = 0; + virtual scpi_result_t read(char *data, size_t *len, size_t max_len) = 0; + virtual void claim_control() = 0; + virtual void release_control() = 0; }; /*! @@ -30,7 +33,7 @@ class VXI_Server }; public: - VXI_Server(SCPI_handler &scpi_handler); + VXI_Server(SCPI_handler_interface &scpi_handler); ~VXI_Server(); void loop(); @@ -56,6 +59,6 @@ class VXI_Server Read_Type read_type; uint32_t rw_channel; cyclic_uint32_t vxi_port; - SCPI_handler &scpi_handler; + SCPI_handler_interface &scpi_handler; }; From 7f84cb8caa49a08d6a6720c3e6ae9e9c5d2c2204 Mon Sep 17 00:00:00 2001 From: hans boot Date: Sun, 23 Feb 2025 20:23:26 +0100 Subject: [PATCH 11/17] improve UI and readme --- README.md | 16 +++++++++++----- src/riden_http_server/riden_http_server.cpp | 6 +++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a8b147d..5016405 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,11 @@ -# **Work in progress, not ready yet** +# **Work in progress, not fully ready yet** + +Status: VXI-11 and raw socket servers are functional and seem robust. + +TODO: + +* error feedback in VXI-11 + # Riden Dongle - A Multi-Protocol Firmware for the Riden WiFi Module @@ -46,15 +53,14 @@ The firmware has been tested with various tools and libraries: The regular Riden power supply firmware is considerably slower than UniSoft, handling less than 10 queries/second. -Raw socket SCPI communication is about 2ms per query faster than VXI-11. -On write operations, there is no speed difference between the 2 styles of communication. - ## VISA communication directives ### VXI-11 The VXI-11 channel (`TCPIP::::INSTR`) is auto discoverable via mDNS, TCP and UDP, making it highly compatible with most tools. +When you use the VXI server, the raw socket server is disabled. + ### Raw sockets Raw socket capability cannot be auto discovered by pyvisa as of now. It can be discovered by lxi tools (see below) @@ -66,7 +72,7 @@ When using the raw sockets (`TCPIP::::5025::SOCKET`), you must, like inst.write_termination = "\n" ``` -Also, be aware that when writing many commands to the device, the network and the device will queue them up. As a result, the delay between the moment your client issues a command, and the moment the device handles the command, can be significant. If you do not want that, insert a sleep of more than 150ms after each write command, forcing the network to send 1 command at a time. +Also, be aware that when writing many commands to the device, the network layers and the device will queue them up. As a result, there can be a significant delay between the moment your client issues a command, and the moment the device handles the command. If you do not want that, insert a sleep of more than 150ms after each write command, forcing the network to send 1 command at a time. VXI-11 does not have this problem, since every command requires an ACK. diff --git a/src/riden_http_server/riden_http_server.cpp b/src/riden_http_server/riden_http_server.cpp index 769671f..3ed76fe 100644 --- a/src/riden_http_server/riden_http_server.cpp +++ b/src/riden_http_server/riden_http_server.cpp @@ -14,7 +14,7 @@ using namespace RidenDongle; -static const String scpi_protocol = "SCPI"; +static const String scpi_protocol = "SCPI RAW"; static const String modbustcp_protocol = "Modbus TCP"; static const String vxi11_protocol = "VXI-11"; static const std::list uart_baudrates = { @@ -496,8 +496,8 @@ void RidenHttpServer::send_services() send_info_row("Modbus TCP Port", String(bridge.port(), 10)); send_info_row("VXI-11 Port", String(vxi_server.port(), 10)); send_info_row("SCPI RAW Port", String(scpi.port(), 10)); - send_info_row("VISA Resource Address 1", vxi_server.get_visa_resource()); - send_info_row("VISA Resource Address 2", scpi.get_visa_resource()); + send_info_row("VISA Resource Address VXI-11", vxi_server.get_visa_resource()); + send_info_row("VISA Resource Address RAW", scpi.get_visa_resource()); server.sendContent(" "); server.sendContent(" "); server.sendContent(" "); From dc1ce4d44693c6386c6cf7333b415b1a31edfcea Mon Sep 17 00:00:00 2001 From: hans boot Date: Sun, 23 Feb 2025 20:44:29 +0100 Subject: [PATCH 12/17] should be OK now --- README.md | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 5016405..198956c 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,3 @@ -# **Work in progress, not fully ready yet** - -Status: VXI-11 and raw socket servers are functional and seem robust. - -TODO: - -* error feedback in VXI-11 - - # Riden Dongle - A Multi-Protocol Firmware for the Riden WiFi Module This is an alternative firmware for the Riden WiFi module that @@ -15,7 +6,7 @@ provides Modbus TCP and SCPI support as well as a web interface. The firmware has been tested with various tools and libraries: - Riden Hardware - - RD6006 + - RD6006 and RD6012 - Riden Firmware - Riden v1.28 - Riden v1.41 @@ -55,11 +46,13 @@ handling less than 10 queries/second. ## VISA communication directives +An example test program can be found under [/scripts/test_pyvisa.py](/scripts/test_pyvisa.py) + ### VXI-11 The VXI-11 channel (`TCPIP::::INSTR`) is auto discoverable via mDNS, TCP and UDP, making it highly compatible with most tools. -When you use the VXI server, the raw socket server is disabled. +While you use the VXI server, the raw socket server is disabled. ### Raw sockets @@ -72,7 +65,7 @@ When using the raw sockets (`TCPIP::::5025::SOCKET`), you must, like inst.write_termination = "\n" ``` -Also, be aware that when writing many commands to the device, the network layers and the device will queue them up. As a result, there can be a significant delay between the moment your client issues a command, and the moment the device handles the command. If you do not want that, insert a sleep of more than 150ms after each write command, forcing the network to send 1 command at a time. +Also, be aware that when writing many commands to the device, the network layers and the device will queue them up. As a result, there can be a significant delay between the moment your client issues a command, and the moment the device handles the command. If you do not want that, insert a sleep of more than 150ms after each write command, forcing the network to send 1 command at a time. (the minimum delay depends on the configuration of your platform) VXI-11 does not have this problem, since every command requires an ACK. From 8c1a0050424dbe175ed514253289aafc815f0437 Mon Sep 17 00:00:00 2001 From: hans boot Date: Mon, 24 Feb 2025 09:09:17 +0100 Subject: [PATCH 13/17] improve cohabitation and reuse --- include/riden_scpi/riden_scpi.h | 5 ++++- src/scpi_bridge/scpi_bridge.h | 4 ++-- src/vxi11_server/vxi_server.cpp | 20 +++++++++++++++++++- src/vxi11_server/vxi_server.h | 7 ++++++- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/include/riden_scpi/riden_scpi.h b/include/riden_scpi/riden_scpi.h index 2936c9c..395e9b6 100644 --- a/include/riden_scpi/riden_scpi.h +++ b/include/riden_scpi/riden_scpi.h @@ -33,7 +33,10 @@ class RidenScpi // some inferface functions to handle commands to the SCPI parser from an outside source // TODO: The SCPI parser should be externalised into another class and instance - void claim_external_control() { external_control = true; } + bool claim_external_control() { + external_control = true; + return true; // I always gain priority + } void release_external_control() { external_control = false; } void write(const char *data, size_t len); scpi_result_t read(char *data, size_t *len, size_t max_len); diff --git a/src/scpi_bridge/scpi_bridge.h b/src/scpi_bridge/scpi_bridge.h index 958c0b9..5ad17d7 100644 --- a/src/scpi_bridge/scpi_bridge.h +++ b/src/scpi_bridge/scpi_bridge.h @@ -16,9 +16,9 @@ class SCPI_handler: public SCPI_handler_interface { return ridenScpi.read(data, len, max_len); } - void claim_control() override + bool claim_control() override { - ridenScpi.claim_external_control(); + return ridenScpi.claim_external_control(); } void release_control() override { diff --git a/src/vxi11_server/vxi_server.cpp b/src/vxi11_server/vxi_server.cpp index aa64214..a41209a 100644 --- a/src/vxi11_server/vxi_server.cpp +++ b/src/vxi11_server/vxi_server.cpp @@ -15,6 +15,15 @@ VXI_Server::VXI_Server(SCPI_handler_interface &scpi_handler) we wait until the begin() command. */ } +VXI_Server::VXI_Server(SCPI_handler_interface &scpi_handler, uint32_t port_min, uint32_t port_max) + : vxi_port(port_min, port_max), + scpi_handler(scpi_handler) +{ + /* We do not start the tcp_server port here, because + WiFi has likely not yet been initialized. Instead, + we wait until the begin() command. */ +} + VXI_Server::~VXI_Server() { } @@ -144,6 +153,16 @@ void VXI_Server::create_link() be null-terminated, but just in case, we will put in the terminator. */ + if (!scpi_handler.claim_control()) { + create_response->rpc_status = rpc::SUCCESS; + create_response->error = rpc::OUT_OF_RESOURCES; // not DEVICE_LOCKED because that would require lock_timeout etc + create_response->link_id = 0; + create_response->abort_port = 0; + create_response->max_receive_size = 0; + send_vxi_packet(client, sizeof(create_response_packet)); + return; + } + create_request->data[create_request->data_len] = 0; LOG_F("CREATE LINK request from \"%s\" on port %u\n", create_request->data, (uint32_t)vxi_port); /* Generate the response */ @@ -153,7 +172,6 @@ void VXI_Server::create_link() create_response->abort_port = 0; create_response->max_receive_size = VXI_READ_SIZE - 4; send_vxi_packet(client, sizeof(create_response_packet)); - scpi_handler.claim_control(); } void VXI_Server::destroy_link() diff --git a/src/vxi11_server/vxi_server.h b/src/vxi11_server/vxi_server.h index ce1a19b..61a08ae 100644 --- a/src/vxi11_server/vxi_server.h +++ b/src/vxi11_server/vxi_server.h @@ -13,9 +13,13 @@ class SCPI_handler_interface { public: virtual ~SCPI_handler_interface() {} + // write a command to the SCPI parser virtual void write(const char *data, size_t len) = 0; + // read a response from the SCPI parser virtual scpi_result_t read(char *data, size_t *len, size_t max_len) = 0; - virtual void claim_control() = 0; + // claim_control() should return true if the SCPI parser is ready to accept a command + virtual bool claim_control() = 0; + // release_control() should be called when the SCPI parser is no longer needed virtual void release_control() = 0; }; @@ -34,6 +38,7 @@ class VXI_Server public: VXI_Server(SCPI_handler_interface &scpi_handler); + VXI_Server(SCPI_handler_interface &scpi_handler, uint32_t port_min, uint32_t port_max); ~VXI_Server(); void loop(); From 439e3f2e681d7e2e0f04a9d3a69319582ad09592 Mon Sep 17 00:00:00 2001 From: hans boot Date: Mon, 24 Feb 2025 19:31:13 +0100 Subject: [PATCH 14/17] unlock socket server when vxi gets force killed --- src/vxi11_server/vxi_server.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vxi11_server/vxi_server.cpp b/src/vxi11_server/vxi_server.cpp index a41209a..acd2ce9 100644 --- a/src/vxi11_server/vxi_server.cpp +++ b/src/vxi11_server/vxi_server.cpp @@ -243,5 +243,6 @@ void VXI_Server::disconnect_client(const IPAddress &ip) { if (client && client.connected() && client.remoteIP() == ip) { client.stop(); + scpi_handler.release_control(); } } From 00183a894284a609f7a119acdfd204f1cc0209da Mon Sep 17 00:00:00 2001 From: hans boot Date: Mon, 24 Feb 2025 19:33:32 +0100 Subject: [PATCH 15/17] readme clarification --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 198956c..be025c4 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ The VXI-11 channel (`TCPIP::::INSTR`) is auto discoverable via mDNS, While you use the VXI server, the raw socket server is disabled. +Note that when you use the web interface to kill a VXI-11 client, it will not properly inform the client. It will just kill the connection. + ### Raw sockets Raw socket capability cannot be auto discovered by pyvisa as of now. It can be discovered by lxi tools (see below) From cf6abe66a2a94bbfc9c36769c95fd4fd469e8773 Mon Sep 17 00:00:00 2001 From: hb020 Date: Tue, 25 Feb 2025 16:38:13 +0100 Subject: [PATCH 16/17] clarify installation methods --- README.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index be025c4..1e0ff12 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ The firmware has been tested with various tools and libraries: - Automatically set power supply clock based on NTP. - mDNS advertising. - Handles approximately 65 queries/second using Modbus TCP or raw socket SCPI - (tested using Unisoft v1.41.1k, UART baudrate set at 9600). + (tested using Unisoft v1.41.1k, UART baudrate set at 921600). ## Warning @@ -95,21 +95,25 @@ as part of the repository. ## Compiling the Firmware -You will need [PlatformIO](https://platformio.org/) in order to -compile the -firmware. +If you want to compile, you will need [PlatformIO](https://platformio.org/) in order to +compile the firmware. No configuration is necessary; simply execute `pio run` and wait. The firmware is located at `.pio/build/esp12e/firmware.bin`. ## Flashing the Firmware -Provided you have prepared the hardware as described, connect -it to your computer as you would when flashing any other ESP12F module. +Provided you have prepared the hardware as described, and have either compiled, or downloaded a binary, +you must connect the dongle to your computer as you would when flashing any other ESP12F module. -Execute +You can use multiple tools to flash the firmware. The most well known are: - pio run -t upload --upload-port +* platformio +* esptool (also available without installation: https://espressif.github.io/esptool-js/) + +Example with PlatformIO: + +```pio run -t upload --upload-port ``` and wait for the firmware to be flashed. @@ -146,7 +150,7 @@ http://RDxxxx-ssssssss.local. Execute the command - lxi discover -m +```lxi discover -m``` to get a list of discovered SCPI devices on the network. This firmware sneakily advertised `lxi` support in order @@ -154,20 +158,20 @@ for lxi-tools to recognise it. Execute the command - lxi scpi -a RDxxxx-ssssssss.local -r "*IDN?" +```lxi scpi -a RDxxxx-ssssssss.local -r "*IDN?"``` to retrieve the SCPI identification string containing power supply model, and firmware version. Execute the command - lxi scpi -a RDxxxx-ssssssss.local -r "VOLT?" +```lxi scpi -a RDxxxx-ssssssss.local -r "VOLT?"``` to retrieve the currently set voltage. Invoke - lxi scpi -a RDxxxx-ssssssss.local -r "VOLT 3.3" +```lxi scpi -a RDxxxx-ssssssss.local -r "VOLT 3.3"``` to set the voltage to 3.3V From 77378ddbdd7c4a53fb7643d2a057e12874141d7a Mon Sep 17 00:00:00 2001 From: hans boot Date: Tue, 25 Feb 2025 19:59:43 +0100 Subject: [PATCH 17/17] added more info in lxi identification --- src/riden_http_server/http_static.h | 4 +++- src/riden_http_server/riden_http_server.cpp | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/riden_http_server/http_static.h b/src/riden_http_server/http_static.h index 7dfd44b..50d61e3 100644 --- a/src/riden_http_server/http_static.h +++ b/src/riden_http_server/http_static.h @@ -184,7 +184,8 @@ static const char *HTML_NO_CONNECTION_BODY = * 5 = Subnet mask * 6 = MAC Address * 7 = IP Gateway - * 8 = VISA Resource Address + * 8 = VISA Resource Address 1 + * 9 = VISA Resource Address 2 */ static const char *LXI_IDENTIFICATION_TEMPLATE = R"==( @@ -196,6 +197,7 @@ static const char *LXI_IDENTIFICATION_TEMPLATE = R"==(http://${3}.local/lxi/identification ${8} + ${9} ${3} ${4} ${5} diff --git a/src/riden_http_server/riden_http_server.cpp b/src/riden_http_server/riden_http_server.cpp index 3ed76fe..683f647 100644 --- a/src/riden_http_server/riden_http_server.cpp +++ b/src/riden_http_server/riden_http_server.cpp @@ -598,6 +598,7 @@ void RidenHttpServer::handle_lxi_identification() subnet_mask.c_str(), mac_address.c_str(), gateway.c_str(), + vxi_server.get_visa_resource(), scpi.get_visa_resource(), 0 // Guard against wrong parameters, such as ${9999} };