Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
11621ad
first crack at test
Relm-Arrowny Mar 12, 2026
6bcc51b
fixing typos
Relm-Arrowny Mar 12, 2026
6d6d19d
add start server
Relm-Arrowny Mar 13, 2026
4835373
added stop
Relm-Arrowny Mar 13, 2026
0a41653
add responses
Relm-Arrowny Mar 13, 2026
c5701cc
add test for timeout
Relm-Arrowny Mar 13, 2026
bbe2f05
add _run_command_loop
Relm-Arrowny Mar 13, 2026
429cd54
rename command loop to serve_client
Relm-Arrowny Mar 13, 2026
061b29d
rename process lline
Relm-Arrowny Mar 13, 2026
5cad303
Merge branch 'main' into 280-baseserver-abstract-class-for-tcp-services
Relm-Arrowny Mar 13, 2026
f516a16
Merge branch 'main' into 280-baseserver-abstract-class-for-tcp-services
Relm-Arrowny Mar 16, 2026
1676c97
correct timeout and allow ipv6
Relm-Arrowny Mar 16, 2026
9821bca
added error testing and full cycle test
Relm-Arrowny Mar 16, 2026
51c2db4
Add minimal docstring
Relm-Arrowny Mar 16, 2026
4fb19a0
add abc to class
Relm-Arrowny Mar 16, 2026
1e582d7
correct test
Relm-Arrowny Mar 16, 2026
4dfc68a
remove redundancy
Relm-Arrowny Mar 16, 2026
7fde156
add the skeleton class for test
Relm-Arrowny Mar 16, 2026
49ea8cf
correct name and add connect_hardware test
Relm-Arrowny Mar 16, 2026
c8d68d5
add usb and connect_hardware
Relm-Arrowny Mar 16, 2026
6e8b9c2
add connect and disconnect
Relm-Arrowny Mar 17, 2026
51aaa83
add exception test
Relm-Arrowny Mar 17, 2026
3f18d4a
add command_registry for Abstract class commands.
Relm-Arrowny Mar 17, 2026
932999b
merge abstract class update
Relm-Arrowny Mar 17, 2026
c05c419
added a _error_helper for logging and responds
Relm-Arrowny Mar 17, 2026
f94cfaf
add helper for error handling
Relm-Arrowny Mar 17, 2026
ac23daa
clean up test
Relm-Arrowny Mar 17, 2026
eef7bd2
clean up test
Relm-Arrowny Mar 17, 2026
9b277a1
make fixture for abstract_instrument_server test
Relm-Arrowny Mar 17, 2026
e1446f9
clean up test
Relm-Arrowny Mar 17, 2026
1b82309
Merge branch '280-baseserver-abstract-class-for-tcp-services' into 28…
Relm-Arrowny Mar 17, 2026
82b4d18
sync branch
Relm-Arrowny Mar 17, 2026
ece821d
Merge branch 'main' into 280-baseserver-abstract-class-for-tcp-services
Relm-Arrowny Mar 19, 2026
8be2714
add some test
Relm-Arrowny Mar 19, 2026
514098d
change response to take byes
Relm-Arrowny Mar 19, 2026
3ea6058
Merge remote-tracking branch 'refs/remotes/origin/280-baseserver-abst…
Relm-Arrowny Mar 19, 2026
c4cd59e
Merge remote-tracking branch 'origin/280-baseserver-abstract-class-fo…
Relm-Arrowny Mar 19, 2026
7a76cf1
add passthrough and absracted hardware command and responds
Relm-Arrowny Mar 19, 2026
df5952f
added typing
Relm-Arrowny Mar 19, 2026
fb8ed3d
add missing test
Relm-Arrowny Mar 19, 2026
4316f98
make use of error_helper
Relm-Arrowny Mar 19, 2026
46cfbdf
move to use readlin and added flush
Relm-Arrowny Mar 19, 2026
8963f96
remove try and catch as it is done by the base class
Relm-Arrowny Mar 19, 2026
11763c3
typo
Relm-Arrowny Mar 19, 2026
1b12db9
add full commond flow test
Relm-Arrowny Mar 19, 2026
1b61822
remove comment
Relm-Arrowny Mar 19, 2026
0938165
add strip to _send_hardware_command
Relm-Arrowny Mar 19, 2026
6ee7409
add None to serial
Relm-Arrowny Mar 19, 2026
a5b19b0
add None to disconnect
Relm-Arrowny Mar 19, 2026
db6014b
add docs
Relm-Arrowny Mar 19, 2026
2b89262
complete test for connection error
Relm-Arrowny Mar 23, 2026
981bbba
move pyserial to optional dependencies
Relm-Arrowny Mar 23, 2026
9872ef1
change args to a list
Relm-Arrowny Mar 24, 2026
1e54ba4
Merge branch '280-baseserver-abstract-class-for-tcp-services' into 28…
Relm-Arrowny Mar 24, 2026
bffd1c2
add contextmanager for hardware not responding
Relm-Arrowny Mar 26, 2026
1c43785
complete hardware_watch test
Relm-Arrowny Mar 26, 2026
6a54bec
Merge branch 'main' into 280-baseserver-abstract-class-for-tcp-services
Relm-Arrowny Mar 26, 2026
8a2c2cc
Merge branch '280-baseserver-abstract-class-for-tcp-services' into 28…
Relm-Arrowny Mar 26, 2026
59602ee
fix sigal required main thread.
Relm-Arrowny Mar 26, 2026
f6a5f42
Merge branch '280-baseserver-abstract-class-for-tcp-services' of gith…
Relm-Arrowny Mar 26, 2026
efb51b6
Merge branch '280-baseserver-abstract-class-for-tcp-services' into 28…
Relm-Arrowny Mar 26, 2026
45daa75
cleanup test
Relm-Arrowny Mar 26, 2026
9ad7454
Merge branch '280-baseserver-abstract-class-for-tcp-services' into 28…
Relm-Arrowny Mar 26, 2026
292918b
change to use time_outcontext instead
Relm-Arrowny Mar 26, 2026
7255cd4
Merge branch '280-baseserver-abstract-class-for-tcp-services' into 28…
Relm-Arrowny Mar 26, 2026
ecf482c
add _check_timeout for sub class to check and raise timeout if it too…
Relm-Arrowny Mar 30, 2026
0920c79
add docs
Relm-Arrowny Mar 30, 2026
ee5d1f0
correct docs
Relm-Arrowny Mar 30, 2026
49db15c
Merge branch '280-baseserver-abstract-class-for-tcp-services' into 28…
Relm-Arrowny Mar 30, 2026
326d8f0
rename docs
Relm-Arrowny Mar 30, 2026
5af5099
Merge remote-tracking branch 'origin/main' into 284-refactor-pulse_ge…
Relm-Arrowny Mar 30, 2026
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
70 changes: 70 additions & 0 deletions docs/how-to/4c_pulse_genertor_shanghai_tech.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Pulse Generator Server (ShanghaiTech)

A TCP/IP instrument server designed to control Pulse Generators via USB Serial (RS232).

## 1. Connection Specifications

| Parameter | Default Value |
| :--- | :--- |
| **Host** | `localhost` (127.0.0.1) |
| **TCP Port** | `8888` |
| **Interface** | USB Serial (COM / /dev/tty) |
| **Baud Rate** | `9600` |
| **Data Protocol** | Tab-Separated, Newline-Terminated (`\t`, `\n`) |

---

## 2. Communication Protocol



The server follows a **Request-Response** model. Every command sent by a client will receive a response starting with a status bit.

### Request Format
`COMMAND` + `\t` (Tab) + `ARGUMENT` (Optional) + `\n` (Newline)

### Response Format
* **Success:** `1` + `\t` + `Data/Message` + `\n`
* **Error:** `0` + `\t` + `Error Description` + `\n`

---

## 3. Command Registry

| Command | Argument | Description | Example |
| :--- | :--- | :--- | :--- |
| `ping` | None | Heartbeat check to verify server status. | `ping\n` |
| `connect_hardware` | None | Initializes/Re-opens the Serial port. | `connect_hardware\n` |
| `set_delay` | `0-1023` | Sets the pulse delay on the hardware. | `set_delay\t512\n` |
| `get_delay` | None | Queries the current delay from hardware. | `get_delay\n` |
| `reset_serial_buffer`| None | Clears the hardware's internal I/O buffers. | `reset_serial_buffer\n` |
| `pass_command` | `string` | Sends a raw AT command to the device. | `pass_command\tAT+VER\n` |
| `shutdown` | None | Safely stops the server and releases hardware. | `shutdown\n` |

---


## 4. Quick Start: Python Client

You can interact with the server using any language that supports sockets. Here is a minimal Python example:

```python
import socket

def send_pulse_command(ip, port, cmd, arg=None):
try:
with socket.create_connection((ip, port), timeout=2.0) as s:
message = f"{cmd}\t{arg}\n" if arg else f"{cmd}\n"
s.sendall(message.encode())
response = s.recv(1024).decode().strip()

status, data = response.split('\t', 1)
if status == '1':
print(f"SUCCESS: {data}")
else:
print(f"ERROR: {data}")
except Exception as e:
print(f"Connection Failed: {e}")

# Example Usage
send_pulse_command("127.0.0.1", 8888, "set_delay", "250")
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@ dependencies = [
"dls-dodal>=2.2.0",
"ophyd-async[sim]",
"scanspec",

]
dynamic = ["version"]
license.file = "LICENSE"
readme = "README.md"
requires-python = ">=3.11"


[project.optional-dependencies]
server = ["pyserial"]
[dependency-groups]
dev = [
"sm_bluesky[server]",
"copier",
"myst-parser",
"pre-commit",
Expand Down
3 changes: 2 additions & 1 deletion src/sm_bluesky/common/server/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .abstract_instrument_server import AbstractInstrumentServer
from .pulse_generator_shanghai_tech import GeneratorServerShanghaiTech

__all__ = ["AbstractInstrumentServer"]
__all__ = ["AbstractInstrumentServer", "GeneratorServerShanghaiTech"]
102 changes: 102 additions & 0 deletions src/sm_bluesky/common/server/pulse_generator_shanghai_tech.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import logging

from serial import Serial

from sm_bluesky.common.server import AbstractInstrumentServer
from sm_bluesky.log import LOGGER


class GeneratorServerShanghaiTech(AbstractInstrumentServer):
def __init__(
self,
host: str,
port: int,
ipv6: bool = False,
usb_port: str = "COM4",
baud_rate: int = 9600,
timeout: float = 1.0,
max_pulse_delay=1024,
):
super().__init__(host, port, ipv6)
self.usb_port: str = usb_port
self.baud_rate: int = baud_rate
self.timeout: float = timeout
self.max_pulse_delay: float = max_pulse_delay
self.device: Serial | None = None

# Expand the registry with Pulse Generator specific commands
self._command_registry.update(
{
b"set_delay": self._set_delay,
b"get_delay": self._get_delay,
b"reset_serial_buffer": self._reset_serial_buffer,
b"pass_command": self._passthrough,
}
)

def connect_hardware(self) -> bool:
"""Initialize the USB connection protocol."""
try:
self.device = Serial(
port=self.usb_port, baudrate=self.baud_rate, timeout=self.timeout
)
self._send_response(b"Hardware connected successfully")
return True
except Exception as e:
self._error_helper(message="Failed to connect to hardware", error=e)
return False

def disconnect_hardware(self):
"""Safely release the USB resource."""
if self.device and self.device.is_open:
try:
self.device.close()
except Exception as e:
self._error_helper(
message="Error occurred while closing hardware connection", error=e
)
self._hardware_connected = False
self.device = None
LOGGER.info("Hardware disconnected successfully")
self._send_response(b"Hardware disconnected")
else:
self._error_helper(
message="Attempted to disconnect hardware that was not connected",
level=logging.WARNING,
)

def _set_delay(self, value: bytes) -> None:
delay = int(value.decode("utf-8"))
if self.max_pulse_delay > delay >= 0:
self._send_hardware_command(b"AT+DLSET=" + value)
LOGGER.info(f"Setting delay to {value}")
else:
raise ValueError(
f"Delay {delay} is out of bounds (0-{self.max_pulse_delay - 1})"
)

def _get_delay(self):
self._send_hardware_command(b"AT+DLSET=?")
LOGGER.info("Reading delay")

def _reset_serial_buffer(self):
if not self.device:
raise ConnectionError(
"Hardware not connected. Call connect_hardware first."
)
self.device.reset_input_buffer()
self.device.reset_output_buffer()
LOGGER.info("Reseting buffers")

def _passthrough(self, value: bytes):
self._send_hardware_command(value)

def _send_hardware_command(self, cmd: bytes) -> None:
if not self.device:
raise ConnectionError(
"Hardware not connected. Call connect_hardware first."
)
self.device.write(cmd + b"\r\n")
self.device.flush()
device_respond = self.device.readline().strip()
self._send_response(device_respond)
Loading
Loading