diff --git a/.gitignore b/.gitignore index e58fb04..207fb58 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,4 @@ Testing/ app-sdk/ vcpkg_installed/ *.desc +__pycache__/ diff --git a/Dockerfile b/Dockerfile index 620ea42..08f8cb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -109,7 +109,7 @@ RUN cd "${VCPKG_ROOT}" && \ # ----------------------------------------------------------------------------- # Install Synapse SDK from internal repository (same steps on both) # ----------------------------------------------------------------------------- -ARG SDK_VERSION=0.4.6 +ARG SDK_VERSION=0.4.8 COPY keys/science-repo-public.asc /usr/share/keyrings/scifi-repo-science-public.asc RUN set -eux; \ apt-get update && apt-get install -y --no-install-recommends ca-certificates; \ diff --git a/README.md b/README.md index 7ed479f..f2cb08a 100644 --- a/README.md +++ b/README.md @@ -117,12 +117,25 @@ for message in tap.stream(): ``` -To listen to joystick output: +## Client Examples + +### Listen to Joystick Output +To listen to joystick output from the FixedWeightDecoder: ```bash python3 ${REPO_ROOT}/client/listen_to_joystick.py --device-ip ``` +### Update Cursor Channels +To dynamically update which channels are used for cursor control: + +```bash +python3 ${REPO_ROOT}/client/update_channels.py --device-ip --channels 0 1 2 3 +``` + +This will send a message to the `set_cursor_channels` tap to update the four channels used for joystick control. The channels must be in the range 0-31. + + ## Development If you want, it is recommended to install and configure pre-commit to auto lint your files. diff --git a/client/requirements.txt b/client/requirements.txt index 7b47ac4..556d25e 100644 --- a/client/requirements.txt +++ b/client/requirements.txt @@ -1,3 +1,3 @@ numpy protobuf -science-synapse +science-synapse>=2.2.7 diff --git a/client/update_channels.py b/client/update_channels.py new file mode 100755 index 0000000..0efc1a9 --- /dev/null +++ b/client/update_channels.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""update_channels.py + +A client that connects to a Synapse device and sends cursor channel updates +to the "set_cursor_channels" tap using ListValue messages with 4 integers. +""" + +import argparse +import sys +import time +from typing import List + +from google.protobuf.struct_pb2 import ListValue, Value +from synapse.client.taps import Tap + + +def parse_args() -> argparse.Namespace: + """CLI argument parser.""" + parser = argparse.ArgumentParser( + description="Send cursor channel updates to a Synapse device" + ) + parser.add_argument( + "--device-ip", required=True, help="IP address of the Synapse device" + ) + parser.add_argument( + "--channels", + nargs=4, + type=int, + required=True, + help="Four channel numbers to set (e.g., --channels 0 1 2 3)", + ) + parser.add_argument( + "--tap-name", + default="set_cursor_channels", + help="Name of the tap to connect to (default: set_cursor_channels)", + ) + return parser.parse_args() + + +def create_channel_list_value(channels: List[int]) -> ListValue: + """Create a ListValue message with the channel numbers. + + Args: + channels: List of 4 channel numbers + + Returns: + ListValue: Protobuf message containing the channel numbers + """ + if len(channels) != 4: + raise ValueError(f"Expected exactly 4 channels, got {len(channels)}") + + # Validate channel range (based on the C++ code validation) + for channel in channels: + if channel < 0 or channel >= 32: + raise ValueError(f"Channel {channel} is out of range (0-31)") + + list_value = ListValue() + for channel in channels: + value = Value() + value.number_value = float(channel) # ListValue uses number_value for numbers + list_value.values.append(value) + + return list_value + + +def main() -> None: + args = parse_args() + + print(f"Connecting to Synapse device at {args.device_ip}") + print(f"Setting cursor channels to: {args.channels}") + + tap = Tap(args.device_ip) + try: + # Connect to the set_cursor_channels tap + if not tap.connect(args.tap_name): + print(f"Failed to connect to tap '{args.tap_name}' at {args.device_ip}", file=sys.stderr) + sys.exit(1) + + print(f"Connected to tap '{args.tap_name}' at {args.device_ip}") + + # Create the ListValue message with the channel numbers + try: + list_value = create_channel_list_value(args.channels) + except ValueError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + # Serialize the message + message_data = list_value.SerializeToString() + + # Send the message + print("Sending channel update...") + if tap.send(message_data): + print(f"Successfully sent cursor channel update: {args.channels}") + else: + print("Failed to send message", file=sys.stderr) + sys.exit(1) + + # Give some time for the message to be processed + time.sleep(0.5) + + except Exception as exc: + print(f"Error: {exc}", file=sys.stderr) + sys.exit(1) + finally: + tap.disconnect() + print("Disconnected from tap") + + +if __name__ == "__main__": + main() diff --git a/src/fixed_weight_decoder.cpp b/src/fixed_weight_decoder.cpp index f2a396e..6bba0ee 100644 --- a/src/fixed_weight_decoder.cpp +++ b/src/fixed_weight_decoder.cpp @@ -29,6 +29,15 @@ bool FixedWeightDecoder::setup() { return false; } + // Setup a consumer tap to listen for reset commands + const auto reset_tap_ret = create_consumer_tap( + "set_cursor_channels", + [this](const google::protobuf::ListValue& message) { handle_update_request(message); }); + if (!reset_tap_ret) { + spdlog::error("Failed to set up consumer tap for set_cursor_channels"); + return false; + } + const uint32_t broadband_node_id = 1; if (!setup_reader(broadband_node_id)) { spdlog::warn("Failed to set up reader for controller"); @@ -346,15 +355,6 @@ bool FixedWeightDecoder::initialize_cursor_channels(const size_t channel_count) return false; } - // Select four random channels - // std::vector all_channels(channel_count); - // std::iota(all_channels.begin(), all_channels.end(), 0); - - // // Randomly sample 4 channels - // std::random_device rd; - // std::mt19937 gen(rd()); - // std::sample(all_channels.begin(), all_channels.end(), cursor_channels_.begin(), 4, gen); - std::stringstream ss; ss << "Using ["; for (const auto& channel : cursor_channels_) { @@ -447,6 +447,44 @@ bool FixedWeightDecoder::parse_config(const synapse::ApplicationNodeConfig& conf return false; } } + +void FixedWeightDecoder::handle_update_request(const google::protobuf::ListValue& message) { + try { + // Just handle the configuration where we want to change the cursor channels + // Need to make sure we have exactly four channels + const auto& values = message.values(); + if (values.size() != 4) { + spdlog::warn("Got a reset request, but didn't see the current number of channels: {}", + message.DebugString()); + return; + } + + // Make sure they are in a good range + for (const auto& value : values) { + if (!value.has_number_value()) { + spdlog::warn("Expected number value for cursor channel"); + return; + } + + const auto channel = value.number_value(); + if (channel < 0 || channel >= 32) { + spdlog::warn("Got an out of range joystick channel: {}", channel); + return; + } + } + + spdlog::info("Got a valid update request, setting new cursor channels"); + { + std::lock_guard lock(cursor_channel_mutex_); + for (size_t i = 0; i < 4; ++i) { + cursor_channels_[i] = values[i].number_value(); + } + } + initialize_cursor_channels(cursor_channels_.size()); + } catch (const std::exception& e) { + spdlog::error("Got a reset request, but had trouble parsing. Why: {}", e.what()); + } +} } // namespace app int main(const int, const char**) { return synapse::Entrypoint(); } diff --git a/src/fixed_weight_decoder.hpp b/src/fixed_weight_decoder.hpp index b464b76..f6a4229 100644 --- a/src/fixed_weight_decoder.hpp +++ b/src/fixed_weight_decoder.hpp @@ -12,6 +12,9 @@ #include "api/datatype.pb.h" #include "api/nodes/broadband_source.pb.h" +// For reset callbacks +#include + namespace app { // 10 hz constexpr auto kPublishRateSec = 1.0 / 10.0; @@ -59,6 +62,7 @@ class FixedWeightDecoder : public synapse::App { spike_count_window_; // Window buffer to store binned spike counts // We will select 4 channels randomly for cursor control + std::mutex cursor_channel_mutex_; std::array cursor_channels_ = {0, 7, 16, 30}; // Should function profiling be enabled? @@ -94,5 +98,11 @@ class FixedWeightDecoder : public synapse::App { // Parse the configuration bool parse_config(const synapse::ApplicationNodeConfig& configuration); + + // If we get a message on our update configuration tap, handle it + // NOTE: if you are expecting frequent updates, you wouldn't handle the data in the callback + // you would instead add the message to a queue and run a process_callback() in your main + // loop + void handle_update_request(const google::protobuf::ListValue& message); }; } // namespace app