Compare commits

...

12 Commits
v1.0 ... master

Author SHA1 Message Date
Joris van Rantwijk 51fb79a9d1 Update user manual
Document scripts puzzle-passwd and puzzle-ntpcfg.
Document command REALTIME?.
2026-03-03 21:08:38 +01:00
Joris van Rantwijk 66444a3067 Bump software version to 1.2 2026-03-01 15:17:59 +01:00
Joris van Rantwijk 4eb6ada765 Add command REALTIME? 2026-03-01 15:17:59 +01:00
Joris van Rantwijk 9687b65b6f Add password management script 2026-03-01 15:17:59 +01:00
Joris van Rantwijk d38617c98f Run init scripts in subshells
Rename custom init scripts so they will run in separate shell processes
rather than being sourced into rcS/rcK.
2026-03-01 15:17:59 +01:00
Joris van Rantwijk 051f24dc2d Add script to configure NTP 2026-03-01 15:17:55 +01:00
Joris van Rantwijk 7fcf233489 Fix typo in IP configuration script 2026-03-01 15:17:39 +01:00
Joris van Rantwijk 2a1cf374b3 Use static NTP server configuration
Ignore NTP servers provided via DHCP.
Enable real-time scheduling for Chrony.
2026-02-08 15:57:58 +01:00
Joris van Rantwijk 2ab072b254 Document commands AIN:CLEAR and TT:CLEAR 2025-10-21 08:44:58 +02:00
Joris van Rantwijk 98058d6291 Bump software version to 1.1 2025-10-21 08:33:17 +02:00
Joris van Rantwijk b32200ba2f Avoid race condition on new data connection
The previous code would clear DMA buffers when a new TCP
data connection is established. This is bad, because the
completion of the server side accept() call happens at
some undefined moment after completion of the client side
connect() call. The client may in the mean time enable
data acquisition via the control connection. This will
lead to data loss if the server subsequently clears
DMA buffers while acquisition is already in progress.

This commit makes two changes:
 - The server does not clear its DMA buffers when it accepts
   a new connection.
 - New commands "AIN:CLEAR" and "TT:CLEAR" are added which
   explicitly clear DMA buffers and drop the current data
   connection, if any.
2025-10-21 08:32:46 +02:00
Joris van Rantwijk 7cf66e031e Reset limit pointer during DMA re-init 2025-10-20 12:19:32 +02:00
21 changed files with 505 additions and 85 deletions

View File

@ -211,7 +211,20 @@ to open the USB serial port of the Red Pitaya.
Set the baud rate to 115200 bps, character format to `8N1`. Set the baud rate to 115200 bps, character format to `8N1`.
Press Enter to get a login prompt on the console. Press Enter to get a login prompt on the console.
Use login `root` with password `root`.
Login as user `root`.
The default password is `root`.
### Changing the password
The default login password is `root`, but this can be changed.
To change the password, login on the console and run the command `puzzle-passwd`.
The tool will prompt to type a new root password.
The changed password will be persistently stored on the SD card.
Do not use the normal Unix command `passwd`.
This tool does not write the new password to the SD card; therefore the changes will be forgotten after reboot.
## Network access ## Network access
@ -241,16 +254,47 @@ This is the same host name as used by the official Red Pitaya software.
It is possible to run an SSH server on the Red Pitaya. It is possible to run an SSH server on the Red Pitaya.
This can be used to remotely log in on the Linux system. This can be used to remotely log in on the Linux system.
To login via SSH, use username `root` with password `root`. To login via SSH, use username `root` and the same password used for logging in via the USB console.
For security reasons, the SSH server is disabled by default. The default password is `root`.
It is recommended to change the password before enabling SSH access.
An SSH server with an easy-to-guess password should never be connected to an untrusted network. An SSH server with an easy-to-guess password should never be connected to an untrusted network.
If you want to use the SSH server, you have to enable it explicitly. For security reasons, the SSH server is disabled by default.
To enable the SSH server, login on the USB console as described above. If you want to login via SSH, it must be enabled explicitly.
Then run the following command: `puzzle-sshcfg enable` . Use the following steps to enable the SSH server:
Finally, run `reboot` to reboot the Red Pitaya.
- Login on the USB console as described above.
- Set a safe password by running command: `puzzle-passwd`
- Enable the SSH server by running command: `puzzle-sshcfg enable`
- Reboot the Red Pitaya by running command: `reboot`
From this point onward, the SSH server will be started automatically during boot. From this point onward, the SSH server will be started automatically during boot.
You can again use the command `puzzle-sshcfg` to disable SSH access.
### NTP configuration
Optionally, an NTP server may be configured to synchronize the system clock via the network.
Without an NTP server, the system clock of the Red Pitaya is initialized to the year 1970 on power-on.
The only feature that uses the system clock is the remote control command `REALTIME?` .
If this command is used, configuring an NTP server is necessary to get correct results.
If this command is not used, synchronizing the system clock is not important.
To configure an NTP server, use the following steps:
- Login on the Red Pitaya via USB console or SSH as described above.
- Run the command `puzzle-ntpcfg server <IP-address>` .
This command configures Chrony to synchronize to the specified NTP server.
The configuration is also written to the SD card and used during boot.
Optionally, a poll interval can be specified as a power of 2 in seconds.
For example: `puzzle-ntpcfg server <IP-address> poll 5` specifies a poll interval of 32 seconds.
Sub-millisecond accuracy can be achieved with an NTP server in the local network and `poll 0` (poll each second).
Poll intervals shorter than 64 seconds must only be used with a dedicated NTP server in the local network.
Using such short poll intervals with a public NTP server is considered abuse.
## Data stream protocol ## Data stream protocol
@ -330,6 +374,7 @@ In the response string, such data elements are separated by space characters.
| `*IDN?` | Instrument identification. | | `*IDN?` | Instrument identification. |
| `RESET` | Restore default settings. | | `RESET` | Restore default settings. |
| `TIMESTAMP?` | Timestamp counter. | | `TIMESTAMP?` | Timestamp counter. |
| `REALTIME?` | System time and timestamp counter. |
| `AIN:CHANNELS:COUNT?` | Number of input channels. | | `AIN:CHANNELS:COUNT?` | Number of input channels. |
| `AIN:CHANNELS:ACTIVE` | Number of active input channels. | | `AIN:CHANNELS:ACTIVE` | Number of active input channels. |
| `AIN:CHn:RANGE` | Analog input range. | | `AIN:CHn:RANGE` | Analog input range. |
@ -351,9 +396,11 @@ In the response string, such data elements are separated by space characters.
| `AIN:TRIGGER:EXT:CHANNEL` | External trigger channel. | | `AIN:TRIGGER:EXT:CHANNEL` | External trigger channel. |
| `AIN:TRIGGER:EXT:EDGE` | External trigger edge. | | `AIN:TRIGGER:EXT:EDGE` | External trigger edge. |
| `AIN:ACQUIRE:ENABLE` | Enable analog acquisition. | | `AIN:ACQUIRE:ENABLE` | Enable analog acquisition. |
| `AIN:CLEAR` | Clear buffer and drop data connection. |
| `TT:SAMPLE?` | Digital input state. | | `TT:SAMPLE?` | Digital input state. |
| `TT:EVENT:MASK` | Timetagger event mask. | | `TT:EVENT:MASK` | Timetagger event mask. |
| `TT:MARK` | Emit timetagger marker. | | `TT:MARK` | Emit timetagger marker. |
| `TT:CLEAR` | Clear buffer and drop data connection. |
| `TEMP:FPGA?` | FPGA temperature. | | `TEMP:FPGA?` | FPGA temperature. |
| `IPCFG[:SAVED]` | IP address configuration. | | `IPCFG[:SAVED]` | IP address configuration. |
| `HALT` | Shut down system. | | `HALT` | Shut down system. |
@ -389,6 +436,23 @@ Any ongoing analog acquisition is stopped.
Query: `TIMESTAMP?` <br> Query: `TIMESTAMP?` <br>
Response: decimal integer, representing the current timestamp in units of 8 ns. Response: decimal integer, representing the current timestamp in units of 8 ns.
### `REALTIME?`
Query: `REALTIME?` <br>
Response: system time and firmware timestamp, separated by a space character.
The system time is represented as a floating point number denoting the number of seconds since 1970-01-01 00:00 UTC (ignoring leap seconds).
The firmware timestamp is represented as a decimal integer denoting the current timestamp in units of 8 ns.
This command is only useful if the Red Pitaya is configured to synchronize its system clock via NTP.
Without NTP, the system time of the Red Pitaya will be incorrect and essentially meaningless.
The system time and firmware timestamp are captured nearly simultaneous, but not exactly simultaneous.
The mismatch between these two time values is almost always less than 10 microseconds.
The overall accuracy of the system time is limited by the synchronization via NTP.
In practice, sub-millisecond accuracy is possible with an NTP server in the local network.
### `AIN:CHANNELS:COUNT?` ### `AIN:CHANNELS:COUNT?`
Query: `AIN:CHANNELS:COUNT?` <br> Query: `AIN:CHANNELS:COUNT?` <br>
@ -626,6 +690,17 @@ When disabled, all triggers are ignored and any ongoing analog acquisition stops
Query: `AIN:ACQUIRE:ENABLE?` <br> Query: `AIN:ACQUIRE:ENABLE?` <br>
Response: either `0` or `1`. Response: either `0` or `1`.
### `AIN:CLEAR`
Command: `AIN:CLEAR`
This command clears internal buffers holding analog acquisition data.
It also drops the current data connection to port 5001, if any.
Clients should send this command just before connecting to the analog
sample data port and enabling analog data acquisition.
This prevents the transmission of stale data from a previous run.
### `TT:SAMPLE?` ### `TT:SAMPLE?`
Query: `TT:SAMPLE?` <br> Query: `TT:SAMPLE?` <br>
@ -662,6 +737,17 @@ Command: `TT:MARK`
This command emits a marker record in the timetagger event stream. This command emits a marker record in the timetagger event stream.
### `TT:CLEAR`
Command: `TT:CLEAR`
This command clears internal buffers holding timetagger data.
It also drops the current data connection to port 5002, if any.
Clients should send this command just before connecting to the timetagger
data port and enabling timetagger events.
This prevents the transmission of stale data from a previous run.
### `TEMP:FPGA?` ### `TEMP:FPGA?`
Query: `TEMP:FPGA?` <br> Query: `TEMP:FPGA?` <br>

View File

@ -1,3 +1,4 @@
include /etc/chrony_dhcp.conf sourcedir /etc/chrony/sources.d
pool pool.ntp.org iburst
makestep 0.1 3 makestep 0.1 3
sched_priority 1
cmdport 0

View File

@ -1,3 +1,7 @@
# Disable chrony (NTP client) # Only start Chrony if an NTP server is configured.
if [ ! -f /etc/chrony/sources.d/ntp.sources ]; then
echo "NTP server not configured, not starting chrony."
exit 1
fi
exit

View File

@ -5,6 +5,21 @@
. /opt/puzzlefw/lib/functions.sh . /opt/puzzlefw/lib/functions.sh
# Copy root password from configuration partition.
copy_passwd() {
# Do nothing if there is no password on the configuration partition.
[ -f ${CONFIG_DIR}/passwd.conf ] || return
# Do nothing if there is no root password entry in the config file.
grep "^root:" ${CONFIG_DIR}/passwd.conf > /etc/shadow.new || return
grep -v "^root:" /etc/shadow >> /etc/shadow.new
chmod 0600 /etc/shadow.new
mv /etc/shadow.new /etc/shadow
}
# Copy SSH host key from configuration partition. # Copy SSH host key from configuration partition.
copy_ssh_host_key() { copy_ssh_host_key() {
@ -25,12 +40,24 @@ copy_ssh_host_key() {
chmod 0600 /etc/dropbear/dropbear_ed25519_host_key || true chmod 0600 /etc/dropbear/dropbear_ed25519_host_key || true
} }
# Take NTP server from configuration partition.
copy_ntp_server() {
# If an NTP server is configured, copy it to Chrony configuration.
if [ -s ${CONFIG_DIR}/ntp.sources ]; then
mkdir -p /etc/chrony/sources.d
cp -p ${CONFIG_DIR}/ntp.sources /etc/chrony/sources.d
fi
}
case "$1" in case "$1" in
start) start)
echo "Reading configuration files from SD card ..." echo "Reading configuration files from SD card ..."
lock_config || exit 1 lock_config || exit 1
read_config || exit 1 read_config || exit 1
copy_passwd
copy_ssh_host_key copy_ssh_host_key
copy_ntp_server
;; ;;
stop|restart|reload) stop|restart|reload)
true true

View File

@ -276,7 +276,7 @@ case "$1" in
*) *)
script="${0##*/}" script="${0##*/}"
cat <<EOF cat <<EOF
Usage: $script {init|show|config|save|restart} Usage: $script {init|show|config|save}
Manage IP address configuration. Manage IP address configuration.

View File

@ -0,0 +1,171 @@
#!/bin/sh
#
# Manage NTP configuration.
#
. /opt/puzzlefw/lib/functions.sh
# Show current configuration.
ntpcfg_show() {
echo "Active NTP configuration:"
if [ -s /etc/chrony/sources.d/ntp.sources ]; then
cat /etc/chrony/sources.d/ntp.sources
else
echo "disabled"
fi
echo
echo "Saved NTP configuration:"
if [ -s ${CONFIG_DIR}/ntp.sources ]; then
cat ${CONFIG_DIR}/ntp.sources
else
echo "disabled"
fi
echo
}
# Check that parameter is a well-formed IPv4 address.
check_ipaddr() {
IFS="." read a b c d <<EOF
$1
EOF
for i in "$a" "$b" "$c" "$d" ; do
if ! [ "$i" -ge 0 -a "$i" -le 255 ]; then
echo "ERROR: Invalid IP address '$1'" >&2
exit 1
fi
done
}
# Configure and enable Chrony.
ntpcfg_server() {
if [ "$1" != "server" ]; then
echo "ERROR: Invalid command '$1'" >&2
exit 1
fi
NTPSERVER="$2"
if [ -z "$NTPSERVER" ]; then
echo "ERROR: Server IP address not specified" >&2
exit 1
fi
check_ipaddr "$NTPSERVER"
POLLOPTS=""
if [ "$#" -gt 2 ]; then
if [ "$3" != "poll" ]; then
echo "ERROR: Unknown option '$3'" >&2
exit 1
fi
POLLINT="$4"
if [ -z "$POLLINT" ]; then
echo "ERROR: Poll interval not specified" >&2
exit 1
fi
if ! [ "$POLLINT" -ge "-1" -a "$POLLINT" -le "10" ]; then
echo "ERROR: Invalid poll interval '$POLLINT', must be between -1 and 10" >&2
exit 1
fi
POLLOPTS="minpoll $POLLINT maxpoll $POLLINT"
if [ "$#" -gt 4 ]; then
echo "ERROR: Unexpected option '$5'" >&2
exit 1
fi
fi
# Lock to avoid conflicting changes.
lock_config || exit 1
echo "Configuring Chrony to start on boot with server $NTPSERVER ..."
SERVERLINE="server $NTPSERVER $POLLOPTS iburst prefer"
echo "$SERVERLINE" > ${CONFIG_DIR}/ntp.sources.new
sync_config ntp.sources || exit 1
mkdir -p /etc/chrony/sources.d
cp -p ${CONFIG_DIR}/ntp.sources /etc/chrony/sources.d
echo
echo "New NTP configuration:"
cat /etc/chrony/sources.d/ntp.sources
echo
echo "Restarting Chrony ..."
/etc/init.d/S49chrony restart
}
# Disable starting Chrony during boot.
ntpcfg_disable() {
# Lock to avoid conflicting changes.
lock_config || exit 1
echo "Disabling Chrony startup on boot ..."
echo -n "" > ${CONFIG_DIR}/ntp.sources.new
sync_config ntp.sources || exit 1
echo "Stopping Chrony ..."
/etc/init.d/S49chrony stop
rm -f /etc/chrony/sources.d/ntp.sources
}
case "$1" in
show)
ntpcfg_show
;;
server)
ntpcfg_server "$@"
;;
disable)
ntpcfg_disable
;;
*)
script="${0##*/}"
cat <<EOF
Usage: $script {server|disable|show}
Manage NTP server configuration.
$script server {IP-address} [poll N]
Enable starting Chrony during boot and specify NTP server to use.
Optionally, the poll interval can be specified as a power of 2 in seconds,
for example "poll 5" means a polling interval of 32 seconds. By default,
the poll interval starts at 6 (64 seconds) and slowly steps up to
10 (1024 seconds).
Sub-millisecond accuracy can be achieved with an NTP server in the local
network and "poll 0" (poll each second).
Do not use a poll interval shorter than 64 seconds unless the NTP server
is in the local network. Doing this with a public NTP server is
considered abuse.
$script disable
Disable starting Chrony during boot and stop Chrony if it was running.
$script show
Display NTP configuration.
EOF
exit 1
;;
esac
exit $?

View File

@ -0,0 +1,32 @@
#!/bin/sh
#
# Password change tool.
#
. /opt/puzzlefw/lib/functions.sh
# Store changed password on the SD card.
store_password() {
# Lock to avoid conflicting changes.
lock_config || exit 1
echo "Writing new password to SD card ..."
grep "^root:" /etc/shadow > ${CONFIG_DIR}/passwd.conf.new
sync_config passwd.conf || exit 1
}
if [ $# -ne 0 ]; then
script="${0##*/}"
cat <<EOF
Usage: $script
Change root password and write the new password hash to the SD card
to make it persistent accross reboot.
EOF
exit 1
fi
passwd && store_password

View File

@ -62,6 +62,11 @@ enable() {
echo "start_ssh=1" > ${CONFIG_DIR}/start_ssh.conf.new echo "start_ssh=1" > ${CONFIG_DIR}/start_ssh.conf.new
sync_config start_ssh.conf || exit 1 sync_config start_ssh.conf || exit 1
echo
echo "NOTE: Please remember to set a non-default root password."
echo " Use 'puzzle-passwd' to change the root password."
} }
# Disable starting SSH server during boot. # Disable starting SSH server during boot.

View File

@ -33,6 +33,7 @@ read_config() {
# Copy config files to RAM filesystem. # Copy config files to RAM filesystem.
cp -a ${CONFIG_MOUNTPOINT}/*.conf $CONFIG_DIR || true cp -a ${CONFIG_MOUNTPOINT}/*.conf $CONFIG_DIR || true
cp -a ${CONFIG_MOUNTPOINT}/ntp.sources $CONFIG_DIR || true
cp -a ${CONFIG_MOUNTPOINT}/dropbear_* $CONFIG_DIR || true cp -a ${CONFIG_MOUNTPOINT}/dropbear_* $CONFIG_DIR || true
umount $CONFIG_MOUNTPOINT umount $CONFIG_MOUNTPOINT

View File

@ -1,29 +0,0 @@
#!/bin/sh
#
# Configure chrony to use NTP server advertised by DHCP server.
#
configure_ntp() {
# Do nothing if the DHCP server does not advertise any NTP server.
[ -z "$ntpsrv" ] && return
# Write config snippet with NTP servers.
for srv in $ntpsrv ; do
echo "server $srv iburst"
done > /etc/chrony_dhcp.conf.new
# Compare to currently configured NTP servers.
if ! diff -q /etc/chrony_dhcp.conf /etc/chrony_dhcp.conf.new >/dev/null ; then
# Update configuration and restart Chrony.
mv /etc/chrony_dhcp.conf.new /etc/chrony_dhcp.conf
/etc/init.d/S49chrony reload
fi
}
case "$1" in
renew|bound)
configure_ntp
;;
esac

View File

@ -115,6 +115,12 @@ public:
asio::ip::tcp::endpoint addr(asio::ip::address_v6::any(), m_tcp_port); asio::ip::tcp::endpoint addr(asio::ip::address_v6::any(), m_tcp_port);
m_acceptor.bind(addr); m_acceptor.bind(addr);
// Clear buffer, then enable DMA stream.
if (! m_connection.is_open()) {
discard_stale_data();
m_dma_stream.set_enabled(true);
}
// Start listening for connections. // Start listening for connections.
m_acceptor.listen(); m_acceptor.listen();
@ -133,9 +139,6 @@ public:
*/ */
void stop_server() void stop_server()
{ {
// Disable DMA stream and clear buffer.
m_dma_stream.init();
// Stop accepting connections. // Stop accepting connections.
if (m_acceptor.is_open()) { if (m_acceptor.is_open()) {
log(LOG_INFO, "Closing TCP server on port %d", m_tcp_port); log(LOG_INFO, "Closing TCP server on port %d", m_tcp_port);
@ -149,6 +152,34 @@ public:
m_stale_receive = true; m_stale_receive = true;
m_stale_send = m_send_in_progress; m_stale_send = m_send_in_progress;
} }
// Disable DMA stream and clear buffer.
m_dma_stream.init();
}
/**
* Disconnect the current client and discard all pending data.
*
* If a multi-threaded Asio event loop is running, this method may only
* be called through the strand returned by "get_executor()".
*/
void clear_data()
{
// Close the current connection.
if (m_connection.is_open()) {
log(LOG_INFO, "Closing connection to port %d", m_tcp_port);
m_connection.close();
m_stale_receive = true;
m_stale_send = m_send_in_progress;
}
// Disable DMA stream and clear buffer.
discard_stale_data();
// Re-enable DMA stream.
if (m_acceptor.is_open()) {
m_dma_stream.set_enabled(true);
}
} }
/** /**
@ -272,10 +303,6 @@ private:
[this](auto ec, size_t n){ handle_receive(ec, n); }); [this](auto ec, size_t n){ handle_receive(ec, n); });
} }
// Clear buffer, then enable DMA stream.
discard_stale_data();
m_dma_stream.set_enabled(true);
// Prepare to send data. // Prepare to send data.
transmit_data(false); transmit_data(false);
} }

View File

@ -732,7 +732,6 @@ public:
// Set up DMA buffer segment. // Set up DMA buffer segment.
m_device.write_reg(m_reg.addr_start, m_buf_start); m_device.write_reg(m_reg.addr_start, m_buf_start);
m_device.write_reg(m_reg.addr_end, m_buf_end); m_device.write_reg(m_reg.addr_end, m_buf_end);
m_device.write_reg(m_reg.addr_limit, m_buf_end - POINTER_MARGIN);
// Initialize DMA stream and reset write pointer. // Initialize DMA stream and reset write pointer.
init(); init();
@ -791,6 +790,9 @@ public:
// Reset read pointer. // Reset read pointer.
m_read_pointer = m_buf_start; m_read_pointer = m_buf_start;
// Reset write limit pointer.
m_device.write_reg(m_reg.addr_limit, m_buf_end - POINTER_MARGIN);
} }
/** Return the number of bytes available in the DMA buffer. */ /** Return the number of bytes available in the DMA buffer. */

View File

@ -12,6 +12,7 @@
#include <math.h> #include <math.h>
#include <stdarg.h> #include <stdarg.h>
#include <stdio.h> #include <stdio.h>
#include <time.h>
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <fstream> #include <fstream>
@ -481,6 +482,8 @@ public:
, m_model_name(model_name) , m_model_name(model_name)
, m_serial_number(serial_number) , m_serial_number(serial_number)
, m_control_server(nullptr) , m_control_server(nullptr)
, m_acquisition_server(nullptr)
, m_timetagger_server(nullptr)
, m_shutting_down(false) , m_shutting_down(false)
, m_exit_status(EXIT_ERROR) , m_exit_status(EXIT_ERROR)
{ } { }
@ -495,10 +498,13 @@ public:
m_control_server = &control_server; m_control_server = &control_server;
} }
/** Register a data server with the command handler. */ /** Register the data servers with the command handler. */
void add_data_server(DataServer& data_server) void set_data_servers(DataServer& acquisition_server,
DataServer& timetagger_server)
{ {
m_data_servers.push_back(&data_server); m_acquisition_server = &acquisition_server;
m_timetagger_server = &timetagger_server;
m_data_servers = {m_acquisition_server, m_timetagger_server};
} }
/** Return the exit status of the command handler. */ /** Return the exit status of the command handler. */
@ -551,17 +557,20 @@ public:
* If a multi-threaded Asio event loop is running, this method may only * If a multi-threaded Asio event loop is running, this method may only
* be called through the strand returned by "get_executor()". * be called through the strand returned by "get_executor()".
* *
* Returns: * The specified response function will be called exactly once to pass
* Response without line terminator, * the command response (without line terminator). An empty string is
* or an empty string if no response must be sent. * passed if no response must be sent.
*
* The call to the response function may be deferred.
*/ */
std::string handle_command(const std::string& command) void handle_command(const std::string& command,
std::function<void(std::string)> respond)
{ {
if (m_shutting_down) { if (m_shutting_down) {
// The server is shutting down. // The server is shutting down.
// Ignore new commands and return an empty string to indicate // Ignore new commands and send no response.
// that no response must be sent. respond(std::string());
return std::string(); return;
} }
// Split words. // Split words.
@ -569,7 +578,8 @@ public:
// Ignore empty command line without response. // Ignore empty command line without response.
if (tokens.empty()) { if (tokens.empty()) {
return std::string(); respond(std::string());
return;
} }
// Convert command to lower case. // Convert command to lower case.
@ -584,7 +594,8 @@ public:
&& action[7] == ':') { && action[7] == ':') {
char channel_digit = action[6]; char channel_digit = action[6];
if (channel_digit < '1' || channel_digit > '4') { if (channel_digit < '1' || channel_digit > '4') {
return err_unknown_command(); respond(err_unknown_command());
return;
} }
env.channel = channel_digit - '1'; env.channel = channel_digit - '1';
action[6] = 'N'; // mark channel index action[6] = 'N'; // mark channel index
@ -622,10 +633,13 @@ public:
const auto it = command_table_no_args.find(action); const auto it = command_table_no_args.find(action);
if (it != command_table_no_args.end()) { if (it != command_table_no_args.end()) {
if (tokens.size() != 1) { if (tokens.size() != 1) {
return err_unexpected_argument(); respond(err_unexpected_argument());
return;
} }
auto func = it->second; auto func = it->second;
return (this->*func)(env); std::string resp = (this->*func)(env);
respond(resp);
return;
} }
} }
@ -634,37 +648,57 @@ public:
const auto it = command_table_one_arg.find(action); const auto it = command_table_one_arg.find(action);
if (it != command_table_one_arg.end()) { if (it != command_table_one_arg.end()) {
if (tokens.size() < 2) { if (tokens.size() < 2) {
return err_missing_argument(); respond(err_missing_argument());
return;
} }
if (tokens.size() > 2) { if (tokens.size() > 2) {
return err_unexpected_argument(); respond(err_unexpected_argument());
return;
} }
auto func = it->second; auto func = it->second;
return (this->*func)(env, tokens[1]); std::string resp = (this->*func)(env, tokens[1]);
respond(resp);
return;
} }
} }
// Handle command IPCFG. // Handle command IPCFG.
if (action == "ipcfg" || action == "ipcfg:saved") { if (action == "ipcfg" || action == "ipcfg:saved") {
if (tokens.size() < 2) { if (tokens.size() < 2) {
return err_missing_argument(); respond(err_missing_argument());
return;
} }
if (tokens.size() > 5) { if (tokens.size() > 5) {
return err_unexpected_argument(); respond(err_unexpected_argument());
return;
} }
tokens.erase(tokens.begin()); tokens.erase(tokens.begin());
return cmd_ipcfg(env, tokens); std::string resp = cmd_ipcfg(env, tokens);
respond(resp);
return;
} }
return err_unknown_command(); // Handle command AIN:CLEAR.
if (action == "ain:clear") {
cmd_ain_clear(respond);
return;
}
// Handle command TT:CLEAR.
if (action == "tt:clear") {
cmd_tt_clear(respond);
return;
}
respond(err_unknown_command());
} }
private: private:
/** Asynchronously stop control and/or data servers. */ /** Asynchronously stop control and data servers. */
void stop_server(std::function<void()> handler); void stop_server(std::function<void()> handler);
void stop_data_servers(unsigned int idx, std::function<void()> handler); void stop_data_servers(unsigned int idx, std::function<void()> handler);
/** Asynchronously start control and/or data servers. */ /** Asynchronously start control and data servers. */
void start_server(); void start_server();
void start_data_servers(unsigned int idx); void start_data_servers(unsigned int idx);
@ -743,6 +777,38 @@ private:
return std::to_string(timestamp); return std::to_string(timestamp);
} }
/** Handle command REALTIME? */
std::string qry_realtime(CommandEnvironment env)
{
struct timespec tp1, tp2, tp3;
uint64_t ts1, ts2, ts3, ts4;
// Read approximately simultaneous real time and timestamp.
ts1 = m_device.get_timestamp();
clock_gettime(CLOCK_REALTIME, &tp1);
ts2 = m_device.get_timestamp();
clock_gettime(CLOCK_REALTIME, &tp2);
ts3 = m_device.get_timestamp();
clock_gettime(CLOCK_REALTIME, &tp3);
ts4 = m_device.get_timestamp();
// Choose shortest interval out of three.
const uint64_t timestamp_mask = 0xffffffffffff;
uint64_t d1 = (ts2 - ts1) & timestamp_mask;
uint64_t d2 = (ts3 - ts2) & timestamp_mask;
uint64_t d3 = (ts4 - ts3) & timestamp_mask;
if (d3 < d1 && d3 < d2) {
ts1 = ts3;
tp1 = tp3;
} else if (d2 < d1) {
ts1 = ts2;
tp1 = tp2;
}
return str_format("%lld.%09ld %llu",
(long long)tp1.tv_sec, (long)tp1.tv_nsec, ts1);
}
/** Handle command AIN:CHANNELS:COUNT? */ /** Handle command AIN:CHANNELS:COUNT? */
std::string qry_channels_count(CommandEnvironment env) std::string qry_channels_count(CommandEnvironment env)
{ {
@ -1365,12 +1431,33 @@ private:
} }
} }
/** Handle command AIN:CLEAR */
void cmd_ain_clear(std::function<void(std::string)> respond)
{
asio::post(m_acquisition_server->get_executor(),
[this,respond]() {
m_acquisition_server->clear_data();
respond("OK");
});
}
/** Handle command TT:CLEAR */
void cmd_tt_clear(std::function<void(std::string)> respond)
{
asio::post(m_timetagger_server->get_executor(),
[this,respond]() {
m_timetagger_server->clear_data();
respond("OK");
});
}
static const inline std::map< static const inline std::map<
std::string, std::string,
std::string (CommandHandler::*)(CommandEnvironment)> std::string (CommandHandler::*)(CommandEnvironment)>
command_table_no_args = { command_table_no_args = {
{ "*idn?", &CommandHandler::qry_idn }, { "*idn?", &CommandHandler::qry_idn },
{ "timestamp?", &CommandHandler::qry_timestamp }, { "timestamp?", &CommandHandler::qry_timestamp },
{ "realtime?", &CommandHandler::qry_realtime },
{ "ain:channels:count?", &CommandHandler::qry_channels_count }, { "ain:channels:count?", &CommandHandler::qry_channels_count },
{ "ain:channels:active?", &CommandHandler::qry_channels_active }, { "ain:channels:active?", &CommandHandler::qry_channels_active },
{ "ain:chN:range?", &CommandHandler::qry_channel_range }, { "ain:chN:range?", &CommandHandler::qry_channel_range },
@ -1436,6 +1523,8 @@ private:
std::string m_model_name; std::string m_model_name;
std::string m_serial_number; std::string m_serial_number;
ControlServer* m_control_server; ControlServer* m_control_server;
DataServer* m_acquisition_server;
DataServer* m_timetagger_server;
std::vector<DataServer*> m_data_servers; std::vector<DataServer*> m_data_servers;
Calibration m_calibration; Calibration m_calibration;
bool m_shutting_down; bool m_shutting_down;
@ -1715,9 +1804,10 @@ private:
m_command_handler.get_executor(), m_command_handler.get_executor(),
[this,conn,command]() { [this,conn,command]() {
// This code runs in the command handler strand. // This code runs in the command handler strand.
// Tell the command handler to run the command.
auto response = m_command_handler.handle_command(command);
// Define helper function which will be called by
// the command handler to pass a response.
auto respond = [this,conn](std::string response) {
// Post the response back to our own strand. // Post the response back to our own strand.
asio::post( asio::post(
m_strand, m_strand,
@ -1726,6 +1816,10 @@ private:
// Handle the response. // Handle the response.
process_response(conn, response); process_response(conn, response);
}); });
};
// Tell the command handler to run the command.
m_command_handler.handle_command(command, respond);
}); });
} }
@ -1878,8 +1972,7 @@ int run_remote_control_server(
ControlServer control_server(io, command_handler, 5025); ControlServer control_server(io, command_handler, 5025);
command_handler.set_control_server(control_server); command_handler.set_control_server(control_server);
command_handler.add_data_server(acq_server); command_handler.set_data_servers(acq_server, timetagger_server);
command_handler.add_data_server(timetagger_server);
// Disable DMA on exit from this function. // Disable DMA on exit from this function.
struct ScopeGuard { struct ScopeGuard {

View File

@ -1,2 +1,2 @@
#define PUZZLEFW_SW_MAJOR 1 #define PUZZLEFW_SW_MAJOR 1
#define PUZZLEFW_SW_MINOR 0 #define PUZZLEFW_SW_MINOR 2