Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,4 @@ Testing/
app-sdk/
vcpkg_installed/
*.desc
__pycache__/
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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; \
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <your-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 <your-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.

Expand Down
2 changes: 1 addition & 1 deletion client/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
numpy
protobuf
science-synapse
science-synapse>=2.2.7
111 changes: 111 additions & 0 deletions client/update_channels.py
Original file line number Diff line number Diff line change
@@ -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()
56 changes: 47 additions & 9 deletions src/fixed_weight_decoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<google::protobuf::ListValue>(
"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");
Expand Down Expand Up @@ -346,15 +355,6 @@ bool FixedWeightDecoder::initialize_cursor_channels(const size_t channel_count)
return false;
}

// Select four random channels
// std::vector<size_t> 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_) {
Expand Down Expand Up @@ -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<std::mutex> 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<app::FixedWeightDecoder>(); }
10 changes: 10 additions & 0 deletions src/fixed_weight_decoder.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
#include "api/datatype.pb.h"
#include "api/nodes/broadband_source.pb.h"

// For reset callbacks
#include <google/protobuf/struct.pb.h>

namespace app {
// 10 hz
constexpr auto kPublishRateSec = 1.0 / 10.0;
Expand Down Expand Up @@ -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<size_t, 4> cursor_channels_ = {0, 7, 16, 30};

// Should function profiling be enabled?
Expand Down Expand Up @@ -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