Skip to content

Repository files navigation

PaceMaker

A PyQt6 desktop GUI for controlling and logging a Druck PACE5000 pressure controller.

Features

  • Connect via TCP/IP (SCPI over port 5025) or Serial (RS-232)
  • Set target pressure and slew rate; switch between Control / Measure modes
  • Manual "Apply" (target field, +/- step buttons) goes through the same slew-rate-verified-before-setpoint path as Scheduled Control / exp_scheduler / the HTTP API (Pace5000Backend.set_pressure_with_ramp()) — a rate typed into the rate field is always sent and read-back-verified together with the target, never assumed to already be correctly set on the device
  • Overshoot suppression (:SOUR:PRES:SLEW:OVER 0) is sent on every connect and is not user-configurable — this app has no scenario where overshoot past the setpoint is acceptable
  • "Pressure reached" requires the reading to stay within tolerance continuously for Pace5000Backend.DEFAULT_STABILITY_DWELL_S (2 s) — a single in-tolerance sample is not enough. Enforced device-side via :SOUR:PRES:INL / :SOUR:PRES:INL:TIME, not user-configurable
  • Effort monitoring: the controller effort (:SOUR:PRES:EFF?, -100 to +100 %) is read every poll and shown live next to the pressure reading. If it stays at/above 90% (either direction) for 30 s continuously, a warning appears explaining the likely cause (supply valve maxed → source pressure/slew rate; vacuum valve maxed → leak/blocked line) — useful for spotting a system that can't actually keep up with the requested ramp
  • Auto Measure-mode safety trip: if the effort drops to/below -50% while in Control mode (the vacuum valve is doing more than half the work), the app automatically switches the device to Measure mode and alerts the operator — a fail-safe against continuing to pull towards -100%
  • Max Safe Pressure: an optional, editable ceiling on the manual target field (and +/- step, and Scheduled Control items), pre-filled from the instrument's control-sensor full-scale (:INST:SENS1:FULL?) on connect but meant to be tightened per-experiment (e.g. to the gasket/diamond's actual safe limit) — not persisted across sessions, since the right value depends on what's currently mounted
  • Live pressure chart with configurable time window
  • CSV data logging
  • Scheduled control: build a sequence of pressure steps and waits, save/load as JSON, and run with live plot and automatic logging
  • HTTP API (standalone mode only): control and monitor the device from another process on the same machine, or another machine on the LAN — see API below

Main window

GUI Main Window

Scheduled control window

GUI scheduled control window

Requirements

  • Python 3.11+
  • PyQt6
  • pyqtgraph
  • pyserial

Install dependencies:

pip install PyQt6 pyqtgraph pyserial

Usage

app.py is the only supported standalone entry point:

python app.py

This works whether this repository (PaceMaker) is cloned on its own, or checked out as the apps/PACE5000/ submodule inside a larger project (e.g. bl18c_controller, run as python apps/PACE5000/app.py from that project's root) — app.py registers this directory as its own private package at runtime rather than assuming any particular parent directory structure, so pace5000_app.py, pace5000_ui_main.py, pace5000_backend.py, and pace5000_api.py always import each other consistently either way.

Connection settings (IP, port, COM port, baud rate) are saved automatically to pace5000_settings.json. The last-used log save directory is also persisted in pace5000_settings.json and restored as the default on next launch.

API

Only available when running app.py standalone (not when this app is embedded in another launcher). Open API → Configure and start API from the menu bar once connected and enable it there, or auto-start it with --api on the command line:

python app.py --api --api-host 0.0.0.0 --api-port 8765 --api-key <key>

Authentication: binding to 127.0.0.1 (the default) requires no API key — only processes on the same machine can reach it. Binding to any other host (e.g. 0.0.0.0 or a specific LAN IP, to allow other machines on the network to reach it) requires an API key, sent as the X-API-Key header on every request. Generate one from the UI ("Regenerate") or pass --api-key; it is persisted in pace5000_settings.json so it stays stable across restarts.

All endpoints are under /api/v1. Request/response bodies are JSON.

MethodPathAuthDescription
GET/api/v1/healthno{"ok": true} — liveness check
GET/api/v1/statusyesCurrent pressure, target, slew rate, control mode, source pressures, controller effort (effort_percent). All pressure/rate fields are always reported in MPa (_mpa suffix) regardless of the unit currently selected in the Manual Control tab (see implementation note below).
POST/api/v1/pressureyesBody {"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min", "token": "..."}. Sets the slew rate (verified) then the setpoint (verified); returns immediately once the device readback confirms the setpoint — does not wait for the pressure to arrive. unit is "MPa" or "Bar"; rate_unit is "MPa/min", "Bar/min", "MPa/sec", or "Bar/sec". token is only required if someone currently holds the lease (see below) — omit it otherwise. 403 if a lease is held and token doesn't match it. 409 if the target exceeds the +ve source pressure, the device's slew rate can't be verified, or the setpoint readback doesn't confirm what was sent (e.g. silently clamped by the device). A 200 response means the device has genuinely accepted both values, not just that the write didn't error.
POST/api/v1/control_modeyesBody {"enabled": true, "token": "..."} — toggle Control/Measure mode. Verified via :OUTP:STAT? readback before responding; 403 if a lease is held and token doesn't match it; 409 if the device doesn't confirm the requested mode after a few retries.
GET/api/v1/pressure/waityesQuery tol, unit, timeout_s (capped at 300 s, must be at least Pace5000Backend.DEFAULT_STABILITY_DWELL_S s or 400). Blocks until the measured pressure has stayed within tol of the current target continuously for DEFAULT_STABILITY_DWELL_S seconds, or returns 408 on timeout. For waits longer than a few tens of seconds, prefer polling /status yourself instead of holding this connection open.
GET/api/v1/leaseyes{"held": bool, "owner": str|null, "expires_in_s": float|null} — current lease status.
POST/api/v1/lease/acquireyesBody {"owner": "my-script", "ttl_s": 60} (ttl_s optional, default 60, capped at 300). Claims exclusive write access to /pressure and /control_mode. 200 {"token": "..."} on success; 409 if someone else already holds it.
POST/api/v1/lease/renewyesBody {"token": "...", "ttl_s": 60}. Extends an unexpired lease you hold before it times out. 409 if the token doesn't match a currently-held lease.
POST/api/v1/lease/releaseyesBody {"token": "..."}. Releases the lease early. 409 if the token doesn't match a currently-held lease.

Example:

curl -H "X-API-Key: $KEY" -X POST http://127.0.0.1:8765/api/v1/pressure \
-H "Content-Type: application/json" \
-d '{"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min"}'
curl -H "X-API-Key: $KEY" http://127.0.0.1:8765/api/v1/status

Implementation note: pressure setting/waiting logic (slew-rate-before-setpoint ordering with read-back verification, and target-reached polling) lives in a single place — Pace5000Backend.set_pressure_with_ramp() / wait_for_pressure() — shared by this API, the manual control tab and Scheduled Control feature in this app, and the external bl18c_controller experimental scheduler that can embed this app. Sending the setpoint before the slew rate is confirmed risks the device approaching the new setpoint at whatever rate was previously in effect. Both the slew rate and the setpoint are read back and verified before set_pressure_with_ramp() returns — a caller (in particular an HTTP client, whose response is the only signal it gets) must not be told the call succeeded for a value the device silently rejected or clamped. set_pressure_with_ramp() takes a unit ("MPa" or "Bar") that determines the device's active :UNIT:PRES for that call — the API/Scheduled Control/exp_scheduler always convert to MPa first, while the manual tab passes the operator's currently selected display unit directly (via Pace5000Backend.set_active_pressure_unit()), since it keeps the device's active unit in sync with that selection.

Dwell-based progress in wait_for_pressure(): the "reached" decision is still made by the device's own :SENS:PRES:INL? flag (continuously in-band for DEFAULT_STABILITY_DWELL_S, see the class-level comment on that constant) — but callers that want to show live progress (Scheduled Control, exp_scheduler) need more than a bare current-vs-target readout, since sitting inside the arrival band isn't "reached" until the dwell time has elapsed. wait_for_pressure()'s on_update callback is therefore called on every poll as on_update(current_mpa, target_mpa, band_elapsed_s, dwell_s), where band_elapsed_s is a client-side, best-effort clock (reset to 0 the instant a sample falls outside tol_mpa, capped at dwell_s) — UI-only, not the authoritative reached signal.

Unit handling for /status: the device's :UNIT:PRES is one shared piece of state — whatever it's currently set to affects every pressure/rate reading, not just the ones a given caller cares about. Rather than forcing :UNIT:PRES MPA on every /status poll (which would fight the manual tab whenever the operator has it set to Bar, and would add two extra round trips per poll), Pace5000Backend tracks the unit last pushed to the device (_active_pressure_unit, kept in sync by every call site that writes :UNIT:PRES) and get_status_mpa() converts the raw readings to MPa client-side using that tracked unit. This is why any code that changes the device's active pressure unit must go through set_active_pressure_unit() rather than writing :UNIT:PRES directly — a direct write would desync the tracked unit and cause /status to report incorrect MPa values.

Owner lease: the API key alone doesn't stop two authenticated clients from racing each other on /pressure / /control_mode — it only proves each one is allowed to talk to the server. /api/v1/lease/* adds a single, server-wide, advisory lease on top: acquire it, get back a token, and include that token in every subsequent /pressure / /control_mode body. Nobody has to use it — a server with no lease held behaves exactly as before (fully backward compatible for the common single-operator case); it only starts rejecting other clients' writes (403) once someone has actually claimed exclusivity. A lease expires on its own after ttl_s (renew with /lease/renew before that if a long-running scan needs to hold it longer) so a client that crashes or loses its connection can't lock the API out indefinitely. This intentionally does not cover the embedded, non-HTTP usage of Pace5000Backend — the manual control tab, Scheduled Control, and the bl18c_controller experimental scheduler (apps/exp_scheduler, which imports Pace5000Backend directly and never goes through this HTTP layer) are entirely outside the lease's reach. Extending exclusivity to that path would need the lease state to live on Pace5000Backend itself rather than on the HTTP server, plus a decision on whether in-process callers should be required to acquire it too — deliberately left as a separate follow-up rather than folded into this HTTP-only version.

Developer

Hiroki Kobayashi (https://orcid.org/0000-0002-3682-7558)

About

Python-based GUI application for controlling Druck PACE5000

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - khsacc/PaceMaker: Python-based GUI application for controlling Druck PACE5000 · GitHub
Skip to content

Repository files navigation

PaceMaker

A PyQt6 desktop GUI for controlling and logging a Druck PACE5000 pressure controller.

Features

  • Connect via TCP/IP (SCPI over port 5025) or Serial (RS-232)
  • Set target pressure and slew rate; switch between Control / Measure modes
  • Manual "Apply" (target field, +/- step buttons) goes through the same slew-rate-verified-before-setpoint path as Scheduled Control / exp_scheduler / the HTTP API (Pace5000Backend.set_pressure_with_ramp()) — a rate typed into the rate field is always sent and read-back-verified together with the target, never assumed to already be correctly set on the device
  • Overshoot suppression (:SOUR:PRES:SLEW:OVER 0) is sent on every connect and is not user-configurable — this app has no scenario where overshoot past the setpoint is acceptable
  • "Pressure reached" requires the reading to stay within tolerance continuously for Pace5000Backend.DEFAULT_STABILITY_DWELL_S (2 s) — a single in-tolerance sample is not enough. Enforced device-side via :SOUR:PRES:INL / :SOUR:PRES:INL:TIME, not user-configurable
  • Effort monitoring: the controller effort (:SOUR:PRES:EFF?, -100 to +100 %) is read every poll and shown live next to the pressure reading. If it stays at/above 90% (either direction) for 30 s continuously, a warning appears explaining the likely cause (supply valve maxed → source pressure/slew rate; vacuum valve maxed → leak/blocked line) — useful for spotting a system that can't actually keep up with the requested ramp
  • Auto Measure-mode safety trip: if the effort drops to/below -50% while in Control mode (the vacuum valve is doing more than half the work), the app automatically switches the device to Measure mode and alerts the operator — a fail-safe against continuing to pull towards -100%
  • Max Safe Pressure: an optional, editable ceiling on the manual target field (and +/- step, and Scheduled Control items), pre-filled from the instrument's control-sensor full-scale (:INST:SENS1:FULL?) on connect but meant to be tightened per-experiment (e.g. to the gasket/diamond's actual safe limit) — not persisted across sessions, since the right value depends on what's currently mounted
  • Live pressure chart with configurable time window
  • CSV data logging
  • Scheduled control: build a sequence of pressure steps and waits, save/load as JSON, and run with live plot and automatic logging
  • HTTP API (standalone mode only): control and monitor the device from another process on the same machine, or another machine on the LAN — see API below

Main window

GUI Main Window

Scheduled control window

GUI scheduled control window

Requirements

  • Python 3.11+
  • PyQt6
  • pyqtgraph
  • pyserial

Install dependencies:

pip install PyQt6 pyqtgraph pyserial

Usage

app.py is the only supported standalone entry point:

python app.py

This works whether this repository (PaceMaker) is cloned on its own, or checked out as the apps/PACE5000/ submodule inside a larger project (e.g. bl18c_controller, run as python apps/PACE5000/app.py from that project's root) — app.py registers this directory as its own private package at runtime rather than assuming any particular parent directory structure, so pace5000_app.py, pace5000_ui_main.py, pace5000_backend.py, and pace5000_api.py always import each other consistently either way.

Connection settings (IP, port, COM port, baud rate) are saved automatically to pace5000_settings.json. The last-used log save directory is also persisted in pace5000_settings.json and restored as the default on next launch.

API

Only available when running app.py standalone (not when this app is embedded in another launcher). Open API → Configure and start API from the menu bar once connected and enable it there, or auto-start it with --api on the command line:

python app.py --api --api-host 0.0.0.0 --api-port 8765 --api-key <key>

Authentication: binding to 127.0.0.1 (the default) requires no API key — only processes on the same machine can reach it. Binding to any other host (e.g. 0.0.0.0 or a specific LAN IP, to allow other machines on the network to reach it) requires an API key, sent as the X-API-Key header on every request. Generate one from the UI ("Regenerate") or pass --api-key; it is persisted in pace5000_settings.json so it stays stable across restarts.

All endpoints are under /api/v1. Request/response bodies are JSON.

MethodPathAuthDescription
GET/api/v1/healthno{"ok": true} — liveness check
GET/api/v1/statusyesCurrent pressure, target, slew rate, control mode, source pressures, controller effort (effort_percent). All pressure/rate fields are always reported in MPa (_mpa suffix) regardless of the unit currently selected in the Manual Control tab (see implementation note below).
POST/api/v1/pressureyesBody {"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min", "token": "..."}. Sets the slew rate (verified) then the setpoint (verified); returns immediately once the device readback confirms the setpoint — does not wait for the pressure to arrive. unit is "MPa" or "Bar"; rate_unit is "MPa/min", "Bar/min", "MPa/sec", or "Bar/sec". token is only required if someone currently holds the lease (see below) — omit it otherwise. 403 if a lease is held and token doesn't match it. 409 if the target exceeds the +ve source pressure, the device's slew rate can't be verified, or the setpoint readback doesn't confirm what was sent (e.g. silently clamped by the device). A 200 response means the device has genuinely accepted both values, not just that the write didn't error.
POST/api/v1/control_modeyesBody {"enabled": true, "token": "..."} — toggle Control/Measure mode. Verified via :OUTP:STAT? readback before responding; 403 if a lease is held and token doesn't match it; 409 if the device doesn't confirm the requested mode after a few retries.
GET/api/v1/pressure/waityesQuery tol, unit, timeout_s (capped at 300 s, must be at least Pace5000Backend.DEFAULT_STABILITY_DWELL_S s or 400). Blocks until the measured pressure has stayed within tol of the current target continuously for DEFAULT_STABILITY_DWELL_S seconds, or returns 408 on timeout. For waits longer than a few tens of seconds, prefer polling /status yourself instead of holding this connection open.
GET/api/v1/leaseyes{"held": bool, "owner": str|null, "expires_in_s": float|null} — current lease status.
POST/api/v1/lease/acquireyesBody {"owner": "my-script", "ttl_s": 60} (ttl_s optional, default 60, capped at 300). Claims exclusive write access to /pressure and /control_mode. 200 {"token": "..."} on success; 409 if someone else already holds it.
POST/api/v1/lease/renewyesBody {"token": "...", "ttl_s": 60}. Extends an unexpired lease you hold before it times out. 409 if the token doesn't match a currently-held lease.
POST/api/v1/lease/releaseyesBody {"token": "..."}. Releases the lease early. 409 if the token doesn't match a currently-held lease.

Example:

curl -H "X-API-Key: $KEY" -X POST http://127.0.0.1:8765/api/v1/pressure \
-H "Content-Type: application/json" \
-d '{"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min"}'
curl -H "X-API-Key: $KEY" http://127.0.0.1:8765/api/v1/status

Implementation note: pressure setting/waiting logic (slew-rate-before-setpoint ordering with read-back verification, and target-reached polling) lives in a single place — Pace5000Backend.set_pressure_with_ramp() / wait_for_pressure() — shared by this API, the manual control tab and Scheduled Control feature in this app, and the external bl18c_controller experimental scheduler that can embed this app. Sending the setpoint before the slew rate is confirmed risks the device approaching the new setpoint at whatever rate was previously in effect. Both the slew rate and the setpoint are read back and verified before set_pressure_with_ramp() returns — a caller (in particular an HTTP client, whose response is the only signal it gets) must not be told the call succeeded for a value the device silently rejected or clamped. set_pressure_with_ramp() takes a unit ("MPa" or "Bar") that determines the device's active :UNIT:PRES for that call — the API/Scheduled Control/exp_scheduler always convert to MPa first, while the manual tab passes the operator's currently selected display unit directly (via Pace5000Backend.set_active_pressure_unit()), since it keeps the device's active unit in sync with that selection.

Dwell-based progress in wait_for_pressure(): the "reached" decision is still made by the device's own :SENS:PRES:INL? flag (continuously in-band for DEFAULT_STABILITY_DWELL_S, see the class-level comment on that constant) — but callers that want to show live progress (Scheduled Control, exp_scheduler) need more than a bare current-vs-target readout, since sitting inside the arrival band isn't "reached" until the dwell time has elapsed. wait_for_pressure()'s on_update callback is therefore called on every poll as on_update(current_mpa, target_mpa, band_elapsed_s, dwell_s), where band_elapsed_s is a client-side, best-effort clock (reset to 0 the instant a sample falls outside tol_mpa, capped at dwell_s) — UI-only, not the authoritative reached signal.

Unit handling for /status: the device's :UNIT:PRES is one shared piece of state — whatever it's currently set to affects every pressure/rate reading, not just the ones a given caller cares about. Rather than forcing :UNIT:PRES MPA on every /status poll (which would fight the manual tab whenever the operator has it set to Bar, and would add two extra round trips per poll), Pace5000Backend tracks the unit last pushed to the device (_active_pressure_unit, kept in sync by every call site that writes :UNIT:PRES) and get_status_mpa() converts the raw readings to MPa client-side using that tracked unit. This is why any code that changes the device's active pressure unit must go through set_active_pressure_unit() rather than writing :UNIT:PRES directly — a direct write would desync the tracked unit and cause /status to report incorrect MPa values.

Owner lease: the API key alone doesn't stop two authenticated clients from racing each other on /pressure / /control_mode — it only proves each one is allowed to talk to the server. /api/v1/lease/* adds a single, server-wide, advisory lease on top: acquire it, get back a token, and include that token in every subsequent /pressure / /control_mode body. Nobody has to use it — a server with no lease held behaves exactly as before (fully backward compatible for the common single-operator case); it only starts rejecting other clients' writes (403) once someone has actually claimed exclusivity. A lease expires on its own after ttl_s (renew with /lease/renew before that if a long-running scan needs to hold it longer) so a client that crashes or loses its connection can't lock the API out indefinitely. This intentionally does not cover the embedded, non-HTTP usage of Pace5000Backend — the manual control tab, Scheduled Control, and the bl18c_controller experimental scheduler (apps/exp_scheduler, which imports Pace5000Backend directly and never goes through this HTTP layer) are entirely outside the lease's reach. Extending exclusivity to that path would need the lease state to live on Pace5000Backend itself rather than on the HTTP server, plus a decision on whether in-process callers should be required to acquire it too — deliberately left as a separate follow-up rather than folded into this HTTP-only version.

Developer

Hiroki Kobayashi (https://orcid.org/0000-0002-3682-7558)

About

Python-based GUI application for controlling Druck PACE5000

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - khsacc/PaceMaker: Python-based GUI application for controlling Druck PACE5000 · GitHub
Skip to content

Repository files navigation

PaceMaker

A PyQt6 desktop GUI for controlling and logging a Druck PACE5000 pressure controller.

Features

  • Connect via TCP/IP (SCPI over port 5025) or Serial (RS-232)
  • Set target pressure and slew rate; switch between Control / Measure modes
  • Manual "Apply" (target field, +/- step buttons) goes through the same slew-rate-verified-before-setpoint path as Scheduled Control / exp_scheduler / the HTTP API (Pace5000Backend.set_pressure_with_ramp()) — a rate typed into the rate field is always sent and read-back-verified together with the target, never assumed to already be correctly set on the device
  • Overshoot suppression (:SOUR:PRES:SLEW:OVER 0) is sent on every connect and is not user-configurable — this app has no scenario where overshoot past the setpoint is acceptable
  • "Pressure reached" requires the reading to stay within tolerance continuously for Pace5000Backend.DEFAULT_STABILITY_DWELL_S (2 s) — a single in-tolerance sample is not enough. Enforced device-side via :SOUR:PRES:INL / :SOUR:PRES:INL:TIME, not user-configurable
  • Effort monitoring: the controller effort (:SOUR:PRES:EFF?, -100 to +100 %) is read every poll and shown live next to the pressure reading. If it stays at/above 90% (either direction) for 30 s continuously, a warning appears explaining the likely cause (supply valve maxed → source pressure/slew rate; vacuum valve maxed → leak/blocked line) — useful for spotting a system that can't actually keep up with the requested ramp
  • Auto Measure-mode safety trip: if the effort drops to/below -50% while in Control mode (the vacuum valve is doing more than half the work), the app automatically switches the device to Measure mode and alerts the operator — a fail-safe against continuing to pull towards -100%
  • Max Safe Pressure: an optional, editable ceiling on the manual target field (and +/- step, and Scheduled Control items), pre-filled from the instrument's control-sensor full-scale (:INST:SENS1:FULL?) on connect but meant to be tightened per-experiment (e.g. to the gasket/diamond's actual safe limit) — not persisted across sessions, since the right value depends on what's currently mounted
  • Live pressure chart with configurable time window
  • CSV data logging
  • Scheduled control: build a sequence of pressure steps and waits, save/load as JSON, and run with live plot and automatic logging
  • HTTP API (standalone mode only): control and monitor the device from another process on the same machine, or another machine on the LAN — see API below

Main window

GUI Main Window

Scheduled control window

GUI scheduled control window

Requirements

  • Python 3.11+
  • PyQt6
  • pyqtgraph
  • pyserial

Install dependencies:

pip install PyQt6 pyqtgraph pyserial

Usage

app.py is the only supported standalone entry point:

python app.py

This works whether this repository (PaceMaker) is cloned on its own, or checked out as the apps/PACE5000/ submodule inside a larger project (e.g. bl18c_controller, run as python apps/PACE5000/app.py from that project's root) — app.py registers this directory as its own private package at runtime rather than assuming any particular parent directory structure, so pace5000_app.py, pace5000_ui_main.py, pace5000_backend.py, and pace5000_api.py always import each other consistently either way.

Connection settings (IP, port, COM port, baud rate) are saved automatically to pace5000_settings.json. The last-used log save directory is also persisted in pace5000_settings.json and restored as the default on next launch.

API

Only available when running app.py standalone (not when this app is embedded in another launcher). Open API → Configure and start API from the menu bar once connected and enable it there, or auto-start it with --api on the command line:

python app.py --api --api-host 0.0.0.0 --api-port 8765 --api-key <key>

Authentication: binding to 127.0.0.1 (the default) requires no API key — only processes on the same machine can reach it. Binding to any other host (e.g. 0.0.0.0 or a specific LAN IP, to allow other machines on the network to reach it) requires an API key, sent as the X-API-Key header on every request. Generate one from the UI ("Regenerate") or pass --api-key; it is persisted in pace5000_settings.json so it stays stable across restarts.

All endpoints are under /api/v1. Request/response bodies are JSON.

MethodPathAuthDescription
GET/api/v1/healthno{"ok": true} — liveness check
GET/api/v1/statusyesCurrent pressure, target, slew rate, control mode, source pressures, controller effort (effort_percent). All pressure/rate fields are always reported in MPa (_mpa suffix) regardless of the unit currently selected in the Manual Control tab (see implementation note below).
POST/api/v1/pressureyesBody {"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min", "token": "..."}. Sets the slew rate (verified) then the setpoint (verified); returns immediately once the device readback confirms the setpoint — does not wait for the pressure to arrive. unit is "MPa" or "Bar"; rate_unit is "MPa/min", "Bar/min", "MPa/sec", or "Bar/sec". token is only required if someone currently holds the lease (see below) — omit it otherwise. 403 if a lease is held and token doesn't match it. 409 if the target exceeds the +ve source pressure, the device's slew rate can't be verified, or the setpoint readback doesn't confirm what was sent (e.g. silently clamped by the device). A 200 response means the device has genuinely accepted both values, not just that the write didn't error.
POST/api/v1/control_modeyesBody {"enabled": true, "token": "..."} — toggle Control/Measure mode. Verified via :OUTP:STAT? readback before responding; 403 if a lease is held and token doesn't match it; 409 if the device doesn't confirm the requested mode after a few retries.
GET/api/v1/pressure/waityesQuery tol, unit, timeout_s (capped at 300 s, must be at least Pace5000Backend.DEFAULT_STABILITY_DWELL_S s or 400). Blocks until the measured pressure has stayed within tol of the current target continuously for DEFAULT_STABILITY_DWELL_S seconds, or returns 408 on timeout. For waits longer than a few tens of seconds, prefer polling /status yourself instead of holding this connection open.
GET/api/v1/leaseyes{"held": bool, "owner": str|null, "expires_in_s": float|null} — current lease status.
POST/api/v1/lease/acquireyesBody {"owner": "my-script", "ttl_s": 60} (ttl_s optional, default 60, capped at 300). Claims exclusive write access to /pressure and /control_mode. 200 {"token": "..."} on success; 409 if someone else already holds it.
POST/api/v1/lease/renewyesBody {"token": "...", "ttl_s": 60}. Extends an unexpired lease you hold before it times out. 409 if the token doesn't match a currently-held lease.
POST/api/v1/lease/releaseyesBody {"token": "..."}. Releases the lease early. 409 if the token doesn't match a currently-held lease.

Example:

curl -H "X-API-Key: $KEY" -X POST http://127.0.0.1:8765/api/v1/pressure \
-H "Content-Type: application/json" \
-d '{"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min"}'
curl -H "X-API-Key: $KEY" http://127.0.0.1:8765/api/v1/status

Implementation note: pressure setting/waiting logic (slew-rate-before-setpoint ordering with read-back verification, and target-reached polling) lives in a single place — Pace5000Backend.set_pressure_with_ramp() / wait_for_pressure() — shared by this API, the manual control tab and Scheduled Control feature in this app, and the external bl18c_controller experimental scheduler that can embed this app. Sending the setpoint before the slew rate is confirmed risks the device approaching the new setpoint at whatever rate was previously in effect. Both the slew rate and the setpoint are read back and verified before set_pressure_with_ramp() returns — a caller (in particular an HTTP client, whose response is the only signal it gets) must not be told the call succeeded for a value the device silently rejected or clamped. set_pressure_with_ramp() takes a unit ("MPa" or "Bar") that determines the device's active :UNIT:PRES for that call — the API/Scheduled Control/exp_scheduler always convert to MPa first, while the manual tab passes the operator's currently selected display unit directly (via Pace5000Backend.set_active_pressure_unit()), since it keeps the device's active unit in sync with that selection.

Dwell-based progress in wait_for_pressure(): the "reached" decision is still made by the device's own :SENS:PRES:INL? flag (continuously in-band for DEFAULT_STABILITY_DWELL_S, see the class-level comment on that constant) — but callers that want to show live progress (Scheduled Control, exp_scheduler) need more than a bare current-vs-target readout, since sitting inside the arrival band isn't "reached" until the dwell time has elapsed. wait_for_pressure()'s on_update callback is therefore called on every poll as on_update(current_mpa, target_mpa, band_elapsed_s, dwell_s), where band_elapsed_s is a client-side, best-effort clock (reset to 0 the instant a sample falls outside tol_mpa, capped at dwell_s) — UI-only, not the authoritative reached signal.

Unit handling for /status: the device's :UNIT:PRES is one shared piece of state — whatever it's currently set to affects every pressure/rate reading, not just the ones a given caller cares about. Rather than forcing :UNIT:PRES MPA on every /status poll (which would fight the manual tab whenever the operator has it set to Bar, and would add two extra round trips per poll), Pace5000Backend tracks the unit last pushed to the device (_active_pressure_unit, kept in sync by every call site that writes :UNIT:PRES) and get_status_mpa() converts the raw readings to MPa client-side using that tracked unit. This is why any code that changes the device's active pressure unit must go through set_active_pressure_unit() rather than writing :UNIT:PRES directly — a direct write would desync the tracked unit and cause /status to report incorrect MPa values.

Owner lease: the API key alone doesn't stop two authenticated clients from racing each other on /pressure / /control_mode — it only proves each one is allowed to talk to the server. /api/v1/lease/* adds a single, server-wide, advisory lease on top: acquire it, get back a token, and include that token in every subsequent /pressure / /control_mode body. Nobody has to use it — a server with no lease held behaves exactly as before (fully backward compatible for the common single-operator case); it only starts rejecting other clients' writes (403) once someone has actually claimed exclusivity. A lease expires on its own after ttl_s (renew with /lease/renew before that if a long-running scan needs to hold it longer) so a client that crashes or loses its connection can't lock the API out indefinitely. This intentionally does not cover the embedded, non-HTTP usage of Pace5000Backend — the manual control tab, Scheduled Control, and the bl18c_controller experimental scheduler (apps/exp_scheduler, which imports Pace5000Backend directly and never goes through this HTTP layer) are entirely outside the lease's reach. Extending exclusivity to that path would need the lease state to live on Pace5000Backend itself rather than on the HTTP server, plus a decision on whether in-process callers should be required to acquire it too — deliberately left as a separate follow-up rather than folded into this HTTP-only version.

Developer

Hiroki Kobayashi (https://orcid.org/0000-0002-3682-7558)

About

Python-based GUI application for controlling Druck PACE5000

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - khsacc/PaceMaker: Python-based GUI application for controlling Druck PACE5000 · GitHub
Skip to content

Repository files navigation

PaceMaker

A PyQt6 desktop GUI for controlling and logging a Druck PACE5000 pressure controller.

Features

  • Connect via TCP/IP (SCPI over port 5025) or Serial (RS-232)
  • Set target pressure and slew rate; switch between Control / Measure modes
  • Manual "Apply" (target field, +/- step buttons) goes through the same slew-rate-verified-before-setpoint path as Scheduled Control / exp_scheduler / the HTTP API (Pace5000Backend.set_pressure_with_ramp()) — a rate typed into the rate field is always sent and read-back-verified together with the target, never assumed to already be correctly set on the device
  • Overshoot suppression (:SOUR:PRES:SLEW:OVER 0) is sent on every connect and is not user-configurable — this app has no scenario where overshoot past the setpoint is acceptable
  • "Pressure reached" requires the reading to stay within tolerance continuously for Pace5000Backend.DEFAULT_STABILITY_DWELL_S (2 s) — a single in-tolerance sample is not enough. Enforced device-side via :SOUR:PRES:INL / :SOUR:PRES:INL:TIME, not user-configurable
  • Effort monitoring: the controller effort (:SOUR:PRES:EFF?, -100 to +100 %) is read every poll and shown live next to the pressure reading. If it stays at/above 90% (either direction) for 30 s continuously, a warning appears explaining the likely cause (supply valve maxed → source pressure/slew rate; vacuum valve maxed → leak/blocked line) — useful for spotting a system that can't actually keep up with the requested ramp
  • Auto Measure-mode safety trip: if the effort drops to/below -50% while in Control mode (the vacuum valve is doing more than half the work), the app automatically switches the device to Measure mode and alerts the operator — a fail-safe against continuing to pull towards -100%
  • Max Safe Pressure: an optional, editable ceiling on the manual target field (and +/- step, and Scheduled Control items), pre-filled from the instrument's control-sensor full-scale (:INST:SENS1:FULL?) on connect but meant to be tightened per-experiment (e.g. to the gasket/diamond's actual safe limit) — not persisted across sessions, since the right value depends on what's currently mounted
  • Live pressure chart with configurable time window
  • CSV data logging
  • Scheduled control: build a sequence of pressure steps and waits, save/load as JSON, and run with live plot and automatic logging
  • HTTP API (standalone mode only): control and monitor the device from another process on the same machine, or another machine on the LAN — see API below

Main window

GUI Main Window

Scheduled control window

GUI scheduled control window

Requirements

  • Python 3.11+
  • PyQt6
  • pyqtgraph
  • pyserial

Install dependencies:

pip install PyQt6 pyqtgraph pyserial

Usage

app.py is the only supported standalone entry point:

python app.py

This works whether this repository (PaceMaker) is cloned on its own, or checked out as the apps/PACE5000/ submodule inside a larger project (e.g. bl18c_controller, run as python apps/PACE5000/app.py from that project's root) — app.py registers this directory as its own private package at runtime rather than assuming any particular parent directory structure, so pace5000_app.py, pace5000_ui_main.py, pace5000_backend.py, and pace5000_api.py always import each other consistently either way.

Connection settings (IP, port, COM port, baud rate) are saved automatically to pace5000_settings.json. The last-used log save directory is also persisted in pace5000_settings.json and restored as the default on next launch.

API

Only available when running app.py standalone (not when this app is embedded in another launcher). Open API → Configure and start API from the menu bar once connected and enable it there, or auto-start it with --api on the command line:

python app.py --api --api-host 0.0.0.0 --api-port 8765 --api-key <key>

Authentication: binding to 127.0.0.1 (the default) requires no API key — only processes on the same machine can reach it. Binding to any other host (e.g. 0.0.0.0 or a specific LAN IP, to allow other machines on the network to reach it) requires an API key, sent as the X-API-Key header on every request. Generate one from the UI ("Regenerate") or pass --api-key; it is persisted in pace5000_settings.json so it stays stable across restarts.

All endpoints are under /api/v1. Request/response bodies are JSON.

MethodPathAuthDescription
GET/api/v1/healthno{"ok": true} — liveness check
GET/api/v1/statusyesCurrent pressure, target, slew rate, control mode, source pressures, controller effort (effort_percent). All pressure/rate fields are always reported in MPa (_mpa suffix) regardless of the unit currently selected in the Manual Control tab (see implementation note below).
POST/api/v1/pressureyesBody {"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min", "token": "..."}. Sets the slew rate (verified) then the setpoint (verified); returns immediately once the device readback confirms the setpoint — does not wait for the pressure to arrive. unit is "MPa" or "Bar"; rate_unit is "MPa/min", "Bar/min", "MPa/sec", or "Bar/sec". token is only required if someone currently holds the lease (see below) — omit it otherwise. 403 if a lease is held and token doesn't match it. 409 if the target exceeds the +ve source pressure, the device's slew rate can't be verified, or the setpoint readback doesn't confirm what was sent (e.g. silently clamped by the device). A 200 response means the device has genuinely accepted both values, not just that the write didn't error.
POST/api/v1/control_modeyesBody {"enabled": true, "token": "..."} — toggle Control/Measure mode. Verified via :OUTP:STAT? readback before responding; 403 if a lease is held and token doesn't match it; 409 if the device doesn't confirm the requested mode after a few retries.
GET/api/v1/pressure/waityesQuery tol, unit, timeout_s (capped at 300 s, must be at least Pace5000Backend.DEFAULT_STABILITY_DWELL_S s or 400). Blocks until the measured pressure has stayed within tol of the current target continuously for DEFAULT_STABILITY_DWELL_S seconds, or returns 408 on timeout. For waits longer than a few tens of seconds, prefer polling /status yourself instead of holding this connection open.
GET/api/v1/leaseyes{"held": bool, "owner": str|null, "expires_in_s": float|null} — current lease status.
POST/api/v1/lease/acquireyesBody {"owner": "my-script", "ttl_s": 60} (ttl_s optional, default 60, capped at 300). Claims exclusive write access to /pressure and /control_mode. 200 {"token": "..."} on success; 409 if someone else already holds it.
POST/api/v1/lease/renewyesBody {"token": "...", "ttl_s": 60}. Extends an unexpired lease you hold before it times out. 409 if the token doesn't match a currently-held lease.
POST/api/v1/lease/releaseyesBody {"token": "..."}. Releases the lease early. 409 if the token doesn't match a currently-held lease.

Example:

curl -H "X-API-Key: $KEY" -X POST http://127.0.0.1:8765/api/v1/pressure \
-H "Content-Type: application/json" \
-d '{"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min"}'
curl -H "X-API-Key: $KEY" http://127.0.0.1:8765/api/v1/status

Implementation note: pressure setting/waiting logic (slew-rate-before-setpoint ordering with read-back verification, and target-reached polling) lives in a single place — Pace5000Backend.set_pressure_with_ramp() / wait_for_pressure() — shared by this API, the manual control tab and Scheduled Control feature in this app, and the external bl18c_controller experimental scheduler that can embed this app. Sending the setpoint before the slew rate is confirmed risks the device approaching the new setpoint at whatever rate was previously in effect. Both the slew rate and the setpoint are read back and verified before set_pressure_with_ramp() returns — a caller (in particular an HTTP client, whose response is the only signal it gets) must not be told the call succeeded for a value the device silently rejected or clamped. set_pressure_with_ramp() takes a unit ("MPa" or "Bar") that determines the device's active :UNIT:PRES for that call — the API/Scheduled Control/exp_scheduler always convert to MPa first, while the manual tab passes the operator's currently selected display unit directly (via Pace5000Backend.set_active_pressure_unit()), since it keeps the device's active unit in sync with that selection.

Dwell-based progress in wait_for_pressure(): the "reached" decision is still made by the device's own :SENS:PRES:INL? flag (continuously in-band for DEFAULT_STABILITY_DWELL_S, see the class-level comment on that constant) — but callers that want to show live progress (Scheduled Control, exp_scheduler) need more than a bare current-vs-target readout, since sitting inside the arrival band isn't "reached" until the dwell time has elapsed. wait_for_pressure()'s on_update callback is therefore called on every poll as on_update(current_mpa, target_mpa, band_elapsed_s, dwell_s), where band_elapsed_s is a client-side, best-effort clock (reset to 0 the instant a sample falls outside tol_mpa, capped at dwell_s) — UI-only, not the authoritative reached signal.

Unit handling for /status: the device's :UNIT:PRES is one shared piece of state — whatever it's currently set to affects every pressure/rate reading, not just the ones a given caller cares about. Rather than forcing :UNIT:PRES MPA on every /status poll (which would fight the manual tab whenever the operator has it set to Bar, and would add two extra round trips per poll), Pace5000Backend tracks the unit last pushed to the device (_active_pressure_unit, kept in sync by every call site that writes :UNIT:PRES) and get_status_mpa() converts the raw readings to MPa client-side using that tracked unit. This is why any code that changes the device's active pressure unit must go through set_active_pressure_unit() rather than writing :UNIT:PRES directly — a direct write would desync the tracked unit and cause /status to report incorrect MPa values.

Owner lease: the API key alone doesn't stop two authenticated clients from racing each other on /pressure / /control_mode — it only proves each one is allowed to talk to the server. /api/v1/lease/* adds a single, server-wide, advisory lease on top: acquire it, get back a token, and include that token in every subsequent /pressure / /control_mode body. Nobody has to use it — a server with no lease held behaves exactly as before (fully backward compatible for the common single-operator case); it only starts rejecting other clients' writes (403) once someone has actually claimed exclusivity. A lease expires on its own after ttl_s (renew with /lease/renew before that if a long-running scan needs to hold it longer) so a client that crashes or loses its connection can't lock the API out indefinitely. This intentionally does not cover the embedded, non-HTTP usage of Pace5000Backend — the manual control tab, Scheduled Control, and the bl18c_controller experimental scheduler (apps/exp_scheduler, which imports Pace5000Backend directly and never goes through this HTTP layer) are entirely outside the lease's reach. Extending exclusivity to that path would need the lease state to live on Pace5000Backend itself rather than on the HTTP server, plus a decision on whether in-process callers should be required to acquire it too — deliberately left as a separate follow-up rather than folded into this HTTP-only version.

Developer

Hiroki Kobayashi (https://orcid.org/0000-0002-3682-7558)

About

Python-based GUI application for controlling Druck PACE5000

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - khsacc/PaceMaker: Python-based GUI application for controlling Druck PACE5000 · GitHub
Skip to content

Repository files navigation

PaceMaker

A PyQt6 desktop GUI for controlling and logging a Druck PACE5000 pressure controller.

Features

  • Connect via TCP/IP (SCPI over port 5025) or Serial (RS-232)
  • Set target pressure and slew rate; switch between Control / Measure modes
  • Manual "Apply" (target field, +/- step buttons) goes through the same slew-rate-verified-before-setpoint path as Scheduled Control / exp_scheduler / the HTTP API (Pace5000Backend.set_pressure_with_ramp()) — a rate typed into the rate field is always sent and read-back-verified together with the target, never assumed to already be correctly set on the device
  • Overshoot suppression (:SOUR:PRES:SLEW:OVER 0) is sent on every connect and is not user-configurable — this app has no scenario where overshoot past the setpoint is acceptable
  • "Pressure reached" requires the reading to stay within tolerance continuously for Pace5000Backend.DEFAULT_STABILITY_DWELL_S (2 s) — a single in-tolerance sample is not enough. Enforced device-side via :SOUR:PRES:INL / :SOUR:PRES:INL:TIME, not user-configurable
  • Effort monitoring: the controller effort (:SOUR:PRES:EFF?, -100 to +100 %) is read every poll and shown live next to the pressure reading. If it stays at/above 90% (either direction) for 30 s continuously, a warning appears explaining the likely cause (supply valve maxed → source pressure/slew rate; vacuum valve maxed → leak/blocked line) — useful for spotting a system that can't actually keep up with the requested ramp
  • Auto Measure-mode safety trip: if the effort drops to/below -50% while in Control mode (the vacuum valve is doing more than half the work), the app automatically switches the device to Measure mode and alerts the operator — a fail-safe against continuing to pull towards -100%
  • Max Safe Pressure: an optional, editable ceiling on the manual target field (and +/- step, and Scheduled Control items), pre-filled from the instrument's control-sensor full-scale (:INST:SENS1:FULL?) on connect but meant to be tightened per-experiment (e.g. to the gasket/diamond's actual safe limit) — not persisted across sessions, since the right value depends on what's currently mounted
  • Live pressure chart with configurable time window
  • CSV data logging
  • Scheduled control: build a sequence of pressure steps and waits, save/load as JSON, and run with live plot and automatic logging
  • HTTP API (standalone mode only): control and monitor the device from another process on the same machine, or another machine on the LAN — see API below

Main window

GUI Main Window

Scheduled control window

GUI scheduled control window

Requirements

  • Python 3.11+
  • PyQt6
  • pyqtgraph
  • pyserial

Install dependencies:

pip install PyQt6 pyqtgraph pyserial

Usage

app.py is the only supported standalone entry point:

python app.py

This works whether this repository (PaceMaker) is cloned on its own, or checked out as the apps/PACE5000/ submodule inside a larger project (e.g. bl18c_controller, run as python apps/PACE5000/app.py from that project's root) — app.py registers this directory as its own private package at runtime rather than assuming any particular parent directory structure, so pace5000_app.py, pace5000_ui_main.py, pace5000_backend.py, and pace5000_api.py always import each other consistently either way.

Connection settings (IP, port, COM port, baud rate) are saved automatically to pace5000_settings.json. The last-used log save directory is also persisted in pace5000_settings.json and restored as the default on next launch.

API

Only available when running app.py standalone (not when this app is embedded in another launcher). Open API → Configure and start API from the menu bar once connected and enable it there, or auto-start it with --api on the command line:

python app.py --api --api-host 0.0.0.0 --api-port 8765 --api-key <key>

Authentication: binding to 127.0.0.1 (the default) requires no API key — only processes on the same machine can reach it. Binding to any other host (e.g. 0.0.0.0 or a specific LAN IP, to allow other machines on the network to reach it) requires an API key, sent as the X-API-Key header on every request. Generate one from the UI ("Regenerate") or pass --api-key; it is persisted in pace5000_settings.json so it stays stable across restarts.

All endpoints are under /api/v1. Request/response bodies are JSON.

MethodPathAuthDescription
GET/api/v1/healthno{"ok": true} — liveness check
GET/api/v1/statusyesCurrent pressure, target, slew rate, control mode, source pressures, controller effort (effort_percent). All pressure/rate fields are always reported in MPa (_mpa suffix) regardless of the unit currently selected in the Manual Control tab (see implementation note below).
POST/api/v1/pressureyesBody {"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min", "token": "..."}. Sets the slew rate (verified) then the setpoint (verified); returns immediately once the device readback confirms the setpoint — does not wait for the pressure to arrive. unit is "MPa" or "Bar"; rate_unit is "MPa/min", "Bar/min", "MPa/sec", or "Bar/sec". token is only required if someone currently holds the lease (see below) — omit it otherwise. 403 if a lease is held and token doesn't match it. 409 if the target exceeds the +ve source pressure, the device's slew rate can't be verified, or the setpoint readback doesn't confirm what was sent (e.g. silently clamped by the device). A 200 response means the device has genuinely accepted both values, not just that the write didn't error.
POST/api/v1/control_modeyesBody {"enabled": true, "token": "..."} — toggle Control/Measure mode. Verified via :OUTP:STAT? readback before responding; 403 if a lease is held and token doesn't match it; 409 if the device doesn't confirm the requested mode after a few retries.
GET/api/v1/pressure/waityesQuery tol, unit, timeout_s (capped at 300 s, must be at least Pace5000Backend.DEFAULT_STABILITY_DWELL_S s or 400). Blocks until the measured pressure has stayed within tol of the current target continuously for DEFAULT_STABILITY_DWELL_S seconds, or returns 408 on timeout. For waits longer than a few tens of seconds, prefer polling /status yourself instead of holding this connection open.
GET/api/v1/leaseyes{"held": bool, "owner": str|null, "expires_in_s": float|null} — current lease status.
POST/api/v1/lease/acquireyesBody {"owner": "my-script", "ttl_s": 60} (ttl_s optional, default 60, capped at 300). Claims exclusive write access to /pressure and /control_mode. 200 {"token": "..."} on success; 409 if someone else already holds it.
POST/api/v1/lease/renewyesBody {"token": "...", "ttl_s": 60}. Extends an unexpired lease you hold before it times out. 409 if the token doesn't match a currently-held lease.
POST/api/v1/lease/releaseyesBody {"token": "..."}. Releases the lease early. 409 if the token doesn't match a currently-held lease.

Example:

curl -H "X-API-Key: $KEY" -X POST http://127.0.0.1:8765/api/v1/pressure \
-H "Content-Type: application/json" \
-d '{"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min"}'
curl -H "X-API-Key: $KEY" http://127.0.0.1:8765/api/v1/status

Implementation note: pressure setting/waiting logic (slew-rate-before-setpoint ordering with read-back verification, and target-reached polling) lives in a single place — Pace5000Backend.set_pressure_with_ramp() / wait_for_pressure() — shared by this API, the manual control tab and Scheduled Control feature in this app, and the external bl18c_controller experimental scheduler that can embed this app. Sending the setpoint before the slew rate is confirmed risks the device approaching the new setpoint at whatever rate was previously in effect. Both the slew rate and the setpoint are read back and verified before set_pressure_with_ramp() returns — a caller (in particular an HTTP client, whose response is the only signal it gets) must not be told the call succeeded for a value the device silently rejected or clamped. set_pressure_with_ramp() takes a unit ("MPa" or "Bar") that determines the device's active :UNIT:PRES for that call — the API/Scheduled Control/exp_scheduler always convert to MPa first, while the manual tab passes the operator's currently selected display unit directly (via Pace5000Backend.set_active_pressure_unit()), since it keeps the device's active unit in sync with that selection.

Dwell-based progress in wait_for_pressure(): the "reached" decision is still made by the device's own :SENS:PRES:INL? flag (continuously in-band for DEFAULT_STABILITY_DWELL_S, see the class-level comment on that constant) — but callers that want to show live progress (Scheduled Control, exp_scheduler) need more than a bare current-vs-target readout, since sitting inside the arrival band isn't "reached" until the dwell time has elapsed. wait_for_pressure()'s on_update callback is therefore called on every poll as on_update(current_mpa, target_mpa, band_elapsed_s, dwell_s), where band_elapsed_s is a client-side, best-effort clock (reset to 0 the instant a sample falls outside tol_mpa, capped at dwell_s) — UI-only, not the authoritative reached signal.

Unit handling for /status: the device's :UNIT:PRES is one shared piece of state — whatever it's currently set to affects every pressure/rate reading, not just the ones a given caller cares about. Rather than forcing :UNIT:PRES MPA on every /status poll (which would fight the manual tab whenever the operator has it set to Bar, and would add two extra round trips per poll), Pace5000Backend tracks the unit last pushed to the device (_active_pressure_unit, kept in sync by every call site that writes :UNIT:PRES) and get_status_mpa() converts the raw readings to MPa client-side using that tracked unit. This is why any code that changes the device's active pressure unit must go through set_active_pressure_unit() rather than writing :UNIT:PRES directly — a direct write would desync the tracked unit and cause /status to report incorrect MPa values.

Owner lease: the API key alone doesn't stop two authenticated clients from racing each other on /pressure / /control_mode — it only proves each one is allowed to talk to the server. /api/v1/lease/* adds a single, server-wide, advisory lease on top: acquire it, get back a token, and include that token in every subsequent /pressure / /control_mode body. Nobody has to use it — a server with no lease held behaves exactly as before (fully backward compatible for the common single-operator case); it only starts rejecting other clients' writes (403) once someone has actually claimed exclusivity. A lease expires on its own after ttl_s (renew with /lease/renew before that if a long-running scan needs to hold it longer) so a client that crashes or loses its connection can't lock the API out indefinitely. This intentionally does not cover the embedded, non-HTTP usage of Pace5000Backend — the manual control tab, Scheduled Control, and the bl18c_controller experimental scheduler (apps/exp_scheduler, which imports Pace5000Backend directly and never goes through this HTTP layer) are entirely outside the lease's reach. Extending exclusivity to that path would need the lease state to live on Pace5000Backend itself rather than on the HTTP server, plus a decision on whether in-process callers should be required to acquire it too — deliberately left as a separate follow-up rather than folded into this HTTP-only version.

Developer

Hiroki Kobayashi (https://orcid.org/0000-0002-3682-7558)

About

Python-based GUI application for controlling Druck PACE5000

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - khsacc/PaceMaker: Python-based GUI application for controlling Druck PACE5000 · GitHub
Skip to content

Repository files navigation

PaceMaker

A PyQt6 desktop GUI for controlling and logging a Druck PACE5000 pressure controller.

Features

  • Connect via TCP/IP (SCPI over port 5025) or Serial (RS-232)
  • Set target pressure and slew rate; switch between Control / Measure modes
  • Manual "Apply" (target field, +/- step buttons) goes through the same slew-rate-verified-before-setpoint path as Scheduled Control / exp_scheduler / the HTTP API (Pace5000Backend.set_pressure_with_ramp()) — a rate typed into the rate field is always sent and read-back-verified together with the target, never assumed to already be correctly set on the device
  • Overshoot suppression (:SOUR:PRES:SLEW:OVER 0) is sent on every connect and is not user-configurable — this app has no scenario where overshoot past the setpoint is acceptable
  • "Pressure reached" requires the reading to stay within tolerance continuously for Pace5000Backend.DEFAULT_STABILITY_DWELL_S (2 s) — a single in-tolerance sample is not enough. Enforced device-side via :SOUR:PRES:INL / :SOUR:PRES:INL:TIME, not user-configurable
  • Effort monitoring: the controller effort (:SOUR:PRES:EFF?, -100 to +100 %) is read every poll and shown live next to the pressure reading. If it stays at/above 90% (either direction) for 30 s continuously, a warning appears explaining the likely cause (supply valve maxed → source pressure/slew rate; vacuum valve maxed → leak/blocked line) — useful for spotting a system that can't actually keep up with the requested ramp
  • Auto Measure-mode safety trip: if the effort drops to/below -50% while in Control mode (the vacuum valve is doing more than half the work), the app automatically switches the device to Measure mode and alerts the operator — a fail-safe against continuing to pull towards -100%
  • Max Safe Pressure: an optional, editable ceiling on the manual target field (and +/- step, and Scheduled Control items), pre-filled from the instrument's control-sensor full-scale (:INST:SENS1:FULL?) on connect but meant to be tightened per-experiment (e.g. to the gasket/diamond's actual safe limit) — not persisted across sessions, since the right value depends on what's currently mounted
  • Live pressure chart with configurable time window
  • CSV data logging
  • Scheduled control: build a sequence of pressure steps and waits, save/load as JSON, and run with live plot and automatic logging
  • HTTP API (standalone mode only): control and monitor the device from another process on the same machine, or another machine on the LAN — see API below

Main window

GUI Main Window

Scheduled control window

GUI scheduled control window

Requirements

  • Python 3.11+
  • PyQt6
  • pyqtgraph
  • pyserial

Install dependencies:

pip install PyQt6 pyqtgraph pyserial

Usage

app.py is the only supported standalone entry point:

python app.py

This works whether this repository (PaceMaker) is cloned on its own, or checked out as the apps/PACE5000/ submodule inside a larger project (e.g. bl18c_controller, run as python apps/PACE5000/app.py from that project's root) — app.py registers this directory as its own private package at runtime rather than assuming any particular parent directory structure, so pace5000_app.py, pace5000_ui_main.py, pace5000_backend.py, and pace5000_api.py always import each other consistently either way.

Connection settings (IP, port, COM port, baud rate) are saved automatically to pace5000_settings.json. The last-used log save directory is also persisted in pace5000_settings.json and restored as the default on next launch.

API

Only available when running app.py standalone (not when this app is embedded in another launcher). Open API → Configure and start API from the menu bar once connected and enable it there, or auto-start it with --api on the command line:

python app.py --api --api-host 0.0.0.0 --api-port 8765 --api-key <key>

Authentication: binding to 127.0.0.1 (the default) requires no API key — only processes on the same machine can reach it. Binding to any other host (e.g. 0.0.0.0 or a specific LAN IP, to allow other machines on the network to reach it) requires an API key, sent as the X-API-Key header on every request. Generate one from the UI ("Regenerate") or pass --api-key; it is persisted in pace5000_settings.json so it stays stable across restarts.

All endpoints are under /api/v1. Request/response bodies are JSON.

MethodPathAuthDescription
GET/api/v1/healthno{"ok": true} — liveness check
GET/api/v1/statusyesCurrent pressure, target, slew rate, control mode, source pressures, controller effort (effort_percent). All pressure/rate fields are always reported in MPa (_mpa suffix) regardless of the unit currently selected in the Manual Control tab (see implementation note below).
POST/api/v1/pressureyesBody {"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min", "token": "..."}. Sets the slew rate (verified) then the setpoint (verified); returns immediately once the device readback confirms the setpoint — does not wait for the pressure to arrive. unit is "MPa" or "Bar"; rate_unit is "MPa/min", "Bar/min", "MPa/sec", or "Bar/sec". token is only required if someone currently holds the lease (see below) — omit it otherwise. 403 if a lease is held and token doesn't match it. 409 if the target exceeds the +ve source pressure, the device's slew rate can't be verified, or the setpoint readback doesn't confirm what was sent (e.g. silently clamped by the device). A 200 response means the device has genuinely accepted both values, not just that the write didn't error.
POST/api/v1/control_modeyesBody {"enabled": true, "token": "..."} — toggle Control/Measure mode. Verified via :OUTP:STAT? readback before responding; 403 if a lease is held and token doesn't match it; 409 if the device doesn't confirm the requested mode after a few retries.
GET/api/v1/pressure/waityesQuery tol, unit, timeout_s (capped at 300 s, must be at least Pace5000Backend.DEFAULT_STABILITY_DWELL_S s or 400). Blocks until the measured pressure has stayed within tol of the current target continuously for DEFAULT_STABILITY_DWELL_S seconds, or returns 408 on timeout. For waits longer than a few tens of seconds, prefer polling /status yourself instead of holding this connection open.
GET/api/v1/leaseyes{"held": bool, "owner": str|null, "expires_in_s": float|null} — current lease status.
POST/api/v1/lease/acquireyesBody {"owner": "my-script", "ttl_s": 60} (ttl_s optional, default 60, capped at 300). Claims exclusive write access to /pressure and /control_mode. 200 {"token": "..."} on success; 409 if someone else already holds it.
POST/api/v1/lease/renewyesBody {"token": "...", "ttl_s": 60}. Extends an unexpired lease you hold before it times out. 409 if the token doesn't match a currently-held lease.
POST/api/v1/lease/releaseyesBody {"token": "..."}. Releases the lease early. 409 if the token doesn't match a currently-held lease.

Example:

curl -H "X-API-Key: $KEY" -X POST http://127.0.0.1:8765/api/v1/pressure \
-H "Content-Type: application/json" \
-d '{"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min"}'
curl -H "X-API-Key: $KEY" http://127.0.0.1:8765/api/v1/status

Implementation note: pressure setting/waiting logic (slew-rate-before-setpoint ordering with read-back verification, and target-reached polling) lives in a single place — Pace5000Backend.set_pressure_with_ramp() / wait_for_pressure() — shared by this API, the manual control tab and Scheduled Control feature in this app, and the external bl18c_controller experimental scheduler that can embed this app. Sending the setpoint before the slew rate is confirmed risks the device approaching the new setpoint at whatever rate was previously in effect. Both the slew rate and the setpoint are read back and verified before set_pressure_with_ramp() returns — a caller (in particular an HTTP client, whose response is the only signal it gets) must not be told the call succeeded for a value the device silently rejected or clamped. set_pressure_with_ramp() takes a unit ("MPa" or "Bar") that determines the device's active :UNIT:PRES for that call — the API/Scheduled Control/exp_scheduler always convert to MPa first, while the manual tab passes the operator's currently selected display unit directly (via Pace5000Backend.set_active_pressure_unit()), since it keeps the device's active unit in sync with that selection.

Dwell-based progress in wait_for_pressure(): the "reached" decision is still made by the device's own :SENS:PRES:INL? flag (continuously in-band for DEFAULT_STABILITY_DWELL_S, see the class-level comment on that constant) — but callers that want to show live progress (Scheduled Control, exp_scheduler) need more than a bare current-vs-target readout, since sitting inside the arrival band isn't "reached" until the dwell time has elapsed. wait_for_pressure()'s on_update callback is therefore called on every poll as on_update(current_mpa, target_mpa, band_elapsed_s, dwell_s), where band_elapsed_s is a client-side, best-effort clock (reset to 0 the instant a sample falls outside tol_mpa, capped at dwell_s) — UI-only, not the authoritative reached signal.

Unit handling for /status: the device's :UNIT:PRES is one shared piece of state — whatever it's currently set to affects every pressure/rate reading, not just the ones a given caller cares about. Rather than forcing :UNIT:PRES MPA on every /status poll (which would fight the manual tab whenever the operator has it set to Bar, and would add two extra round trips per poll), Pace5000Backend tracks the unit last pushed to the device (_active_pressure_unit, kept in sync by every call site that writes :UNIT:PRES) and get_status_mpa() converts the raw readings to MPa client-side using that tracked unit. This is why any code that changes the device's active pressure unit must go through set_active_pressure_unit() rather than writing :UNIT:PRES directly — a direct write would desync the tracked unit and cause /status to report incorrect MPa values.

Owner lease: the API key alone doesn't stop two authenticated clients from racing each other on /pressure / /control_mode — it only proves each one is allowed to talk to the server. /api/v1/lease/* adds a single, server-wide, advisory lease on top: acquire it, get back a token, and include that token in every subsequent /pressure / /control_mode body. Nobody has to use it — a server with no lease held behaves exactly as before (fully backward compatible for the common single-operator case); it only starts rejecting other clients' writes (403) once someone has actually claimed exclusivity. A lease expires on its own after ttl_s (renew with /lease/renew before that if a long-running scan needs to hold it longer) so a client that crashes or loses its connection can't lock the API out indefinitely. This intentionally does not cover the embedded, non-HTTP usage of Pace5000Backend — the manual control tab, Scheduled Control, and the bl18c_controller experimental scheduler (apps/exp_scheduler, which imports Pace5000Backend directly and never goes through this HTTP layer) are entirely outside the lease's reach. Extending exclusivity to that path would need the lease state to live on Pace5000Backend itself rather than on the HTTP server, plus a decision on whether in-process callers should be required to acquire it too — deliberately left as a separate follow-up rather than folded into this HTTP-only version.

Developer

Hiroki Kobayashi (https://orcid.org/0000-0002-3682-7558)

About

Python-based GUI application for controlling Druck PACE5000

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - khsacc/PaceMaker: Python-based GUI application for controlling Druck PACE5000 · GitHub
Skip to content

Repository files navigation

PaceMaker

A PyQt6 desktop GUI for controlling and logging a Druck PACE5000 pressure controller.

Features

  • Connect via TCP/IP (SCPI over port 5025) or Serial (RS-232)
  • Set target pressure and slew rate; switch between Control / Measure modes
  • Manual "Apply" (target field, +/- step buttons) goes through the same slew-rate-verified-before-setpoint path as Scheduled Control / exp_scheduler / the HTTP API (Pace5000Backend.set_pressure_with_ramp()) — a rate typed into the rate field is always sent and read-back-verified together with the target, never assumed to already be correctly set on the device
  • Overshoot suppression (:SOUR:PRES:SLEW:OVER 0) is sent on every connect and is not user-configurable — this app has no scenario where overshoot past the setpoint is acceptable
  • "Pressure reached" requires the reading to stay within tolerance continuously for Pace5000Backend.DEFAULT_STABILITY_DWELL_S (2 s) — a single in-tolerance sample is not enough. Enforced device-side via :SOUR:PRES:INL / :SOUR:PRES:INL:TIME, not user-configurable
  • Effort monitoring: the controller effort (:SOUR:PRES:EFF?, -100 to +100 %) is read every poll and shown live next to the pressure reading. If it stays at/above 90% (either direction) for 30 s continuously, a warning appears explaining the likely cause (supply valve maxed → source pressure/slew rate; vacuum valve maxed → leak/blocked line) — useful for spotting a system that can't actually keep up with the requested ramp
  • Auto Measure-mode safety trip: if the effort drops to/below -50% while in Control mode (the vacuum valve is doing more than half the work), the app automatically switches the device to Measure mode and alerts the operator — a fail-safe against continuing to pull towards -100%
  • Max Safe Pressure: an optional, editable ceiling on the manual target field (and +/- step, and Scheduled Control items), pre-filled from the instrument's control-sensor full-scale (:INST:SENS1:FULL?) on connect but meant to be tightened per-experiment (e.g. to the gasket/diamond's actual safe limit) — not persisted across sessions, since the right value depends on what's currently mounted
  • Live pressure chart with configurable time window
  • CSV data logging
  • Scheduled control: build a sequence of pressure steps and waits, save/load as JSON, and run with live plot and automatic logging
  • HTTP API (standalone mode only): control and monitor the device from another process on the same machine, or another machine on the LAN — see API below

Main window

GUI Main Window

Scheduled control window

GUI scheduled control window

Requirements

  • Python 3.11+
  • PyQt6
  • pyqtgraph
  • pyserial

Install dependencies:

pip install PyQt6 pyqtgraph pyserial

Usage

app.py is the only supported standalone entry point:

python app.py

This works whether this repository (PaceMaker) is cloned on its own, or checked out as the apps/PACE5000/ submodule inside a larger project (e.g. bl18c_controller, run as python apps/PACE5000/app.py from that project's root) — app.py registers this directory as its own private package at runtime rather than assuming any particular parent directory structure, so pace5000_app.py, pace5000_ui_main.py, pace5000_backend.py, and pace5000_api.py always import each other consistently either way.

Connection settings (IP, port, COM port, baud rate) are saved automatically to pace5000_settings.json. The last-used log save directory is also persisted in pace5000_settings.json and restored as the default on next launch.

API

Only available when running app.py standalone (not when this app is embedded in another launcher). Open API → Configure and start API from the menu bar once connected and enable it there, or auto-start it with --api on the command line:

python app.py --api --api-host 0.0.0.0 --api-port 8765 --api-key <key>

Authentication: binding to 127.0.0.1 (the default) requires no API key — only processes on the same machine can reach it. Binding to any other host (e.g. 0.0.0.0 or a specific LAN IP, to allow other machines on the network to reach it) requires an API key, sent as the X-API-Key header on every request. Generate one from the UI ("Regenerate") or pass --api-key; it is persisted in pace5000_settings.json so it stays stable across restarts.

All endpoints are under /api/v1. Request/response bodies are JSON.

MethodPathAuthDescription
GET/api/v1/healthno{"ok": true} — liveness check
GET/api/v1/statusyesCurrent pressure, target, slew rate, control mode, source pressures, controller effort (effort_percent). All pressure/rate fields are always reported in MPa (_mpa suffix) regardless of the unit currently selected in the Manual Control tab (see implementation note below).
POST/api/v1/pressureyesBody {"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min", "token": "..."}. Sets the slew rate (verified) then the setpoint (verified); returns immediately once the device readback confirms the setpoint — does not wait for the pressure to arrive. unit is "MPa" or "Bar"; rate_unit is "MPa/min", "Bar/min", "MPa/sec", or "Bar/sec". token is only required if someone currently holds the lease (see below) — omit it otherwise. 403 if a lease is held and token doesn't match it. 409 if the target exceeds the +ve source pressure, the device's slew rate can't be verified, or the setpoint readback doesn't confirm what was sent (e.g. silently clamped by the device). A 200 response means the device has genuinely accepted both values, not just that the write didn't error.
POST/api/v1/control_modeyesBody {"enabled": true, "token": "..."} — toggle Control/Measure mode. Verified via :OUTP:STAT? readback before responding; 403 if a lease is held and token doesn't match it; 409 if the device doesn't confirm the requested mode after a few retries.
GET/api/v1/pressure/waityesQuery tol, unit, timeout_s (capped at 300 s, must be at least Pace5000Backend.DEFAULT_STABILITY_DWELL_S s or 400). Blocks until the measured pressure has stayed within tol of the current target continuously for DEFAULT_STABILITY_DWELL_S seconds, or returns 408 on timeout. For waits longer than a few tens of seconds, prefer polling /status yourself instead of holding this connection open.
GET/api/v1/leaseyes{"held": bool, "owner": str|null, "expires_in_s": float|null} — current lease status.
POST/api/v1/lease/acquireyesBody {"owner": "my-script", "ttl_s": 60} (ttl_s optional, default 60, capped at 300). Claims exclusive write access to /pressure and /control_mode. 200 {"token": "..."} on success; 409 if someone else already holds it.
POST/api/v1/lease/renewyesBody {"token": "...", "ttl_s": 60}. Extends an unexpired lease you hold before it times out. 409 if the token doesn't match a currently-held lease.
POST/api/v1/lease/releaseyesBody {"token": "..."}. Releases the lease early. 409 if the token doesn't match a currently-held lease.

Example:

curl -H "X-API-Key: $KEY" -X POST http://127.0.0.1:8765/api/v1/pressure \
-H "Content-Type: application/json" \
-d '{"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min"}'
curl -H "X-API-Key: $KEY" http://127.0.0.1:8765/api/v1/status

Implementation note: pressure setting/waiting logic (slew-rate-before-setpoint ordering with read-back verification, and target-reached polling) lives in a single place — Pace5000Backend.set_pressure_with_ramp() / wait_for_pressure() — shared by this API, the manual control tab and Scheduled Control feature in this app, and the external bl18c_controller experimental scheduler that can embed this app. Sending the setpoint before the slew rate is confirmed risks the device approaching the new setpoint at whatever rate was previously in effect. Both the slew rate and the setpoint are read back and verified before set_pressure_with_ramp() returns — a caller (in particular an HTTP client, whose response is the only signal it gets) must not be told the call succeeded for a value the device silently rejected or clamped. set_pressure_with_ramp() takes a unit ("MPa" or "Bar") that determines the device's active :UNIT:PRES for that call — the API/Scheduled Control/exp_scheduler always convert to MPa first, while the manual tab passes the operator's currently selected display unit directly (via Pace5000Backend.set_active_pressure_unit()), since it keeps the device's active unit in sync with that selection.

Dwell-based progress in wait_for_pressure(): the "reached" decision is still made by the device's own :SENS:PRES:INL? flag (continuously in-band for DEFAULT_STABILITY_DWELL_S, see the class-level comment on that constant) — but callers that want to show live progress (Scheduled Control, exp_scheduler) need more than a bare current-vs-target readout, since sitting inside the arrival band isn't "reached" until the dwell time has elapsed. wait_for_pressure()'s on_update callback is therefore called on every poll as on_update(current_mpa, target_mpa, band_elapsed_s, dwell_s), where band_elapsed_s is a client-side, best-effort clock (reset to 0 the instant a sample falls outside tol_mpa, capped at dwell_s) — UI-only, not the authoritative reached signal.

Unit handling for /status: the device's :UNIT:PRES is one shared piece of state — whatever it's currently set to affects every pressure/rate reading, not just the ones a given caller cares about. Rather than forcing :UNIT:PRES MPA on every /status poll (which would fight the manual tab whenever the operator has it set to Bar, and would add two extra round trips per poll), Pace5000Backend tracks the unit last pushed to the device (_active_pressure_unit, kept in sync by every call site that writes :UNIT:PRES) and get_status_mpa() converts the raw readings to MPa client-side using that tracked unit. This is why any code that changes the device's active pressure unit must go through set_active_pressure_unit() rather than writing :UNIT:PRES directly — a direct write would desync the tracked unit and cause /status to report incorrect MPa values.

Owner lease: the API key alone doesn't stop two authenticated clients from racing each other on /pressure / /control_mode — it only proves each one is allowed to talk to the server. /api/v1/lease/* adds a single, server-wide, advisory lease on top: acquire it, get back a token, and include that token in every subsequent /pressure / /control_mode body. Nobody has to use it — a server with no lease held behaves exactly as before (fully backward compatible for the common single-operator case); it only starts rejecting other clients' writes (403) once someone has actually claimed exclusivity. A lease expires on its own after ttl_s (renew with /lease/renew before that if a long-running scan needs to hold it longer) so a client that crashes or loses its connection can't lock the API out indefinitely. This intentionally does not cover the embedded, non-HTTP usage of Pace5000Backend — the manual control tab, Scheduled Control, and the bl18c_controller experimental scheduler (apps/exp_scheduler, which imports Pace5000Backend directly and never goes through this HTTP layer) are entirely outside the lease's reach. Extending exclusivity to that path would need the lease state to live on Pace5000Backend itself rather than on the HTTP server, plus a decision on whether in-process callers should be required to acquire it too — deliberately left as a separate follow-up rather than folded into this HTTP-only version.

Developer

Hiroki Kobayashi (https://orcid.org/0000-0002-3682-7558)

About

Python-based GUI application for controlling Druck PACE5000

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - khsacc/PaceMaker: Python-based GUI application for controlling Druck PACE5000 · GitHub
Skip to content

Repository files navigation

PaceMaker

A PyQt6 desktop GUI for controlling and logging a Druck PACE5000 pressure controller.

Features

  • Connect via TCP/IP (SCPI over port 5025) or Serial (RS-232)
  • Set target pressure and slew rate; switch between Control / Measure modes
  • Manual "Apply" (target field, +/- step buttons) goes through the same slew-rate-verified-before-setpoint path as Scheduled Control / exp_scheduler / the HTTP API (Pace5000Backend.set_pressure_with_ramp()) — a rate typed into the rate field is always sent and read-back-verified together with the target, never assumed to already be correctly set on the device
  • Overshoot suppression (:SOUR:PRES:SLEW:OVER 0) is sent on every connect and is not user-configurable — this app has no scenario where overshoot past the setpoint is acceptable
  • "Pressure reached" requires the reading to stay within tolerance continuously for Pace5000Backend.DEFAULT_STABILITY_DWELL_S (2 s) — a single in-tolerance sample is not enough. Enforced device-side via :SOUR:PRES:INL / :SOUR:PRES:INL:TIME, not user-configurable
  • Effort monitoring: the controller effort (:SOUR:PRES:EFF?, -100 to +100 %) is read every poll and shown live next to the pressure reading. If it stays at/above 90% (either direction) for 30 s continuously, a warning appears explaining the likely cause (supply valve maxed → source pressure/slew rate; vacuum valve maxed → leak/blocked line) — useful for spotting a system that can't actually keep up with the requested ramp
  • Auto Measure-mode safety trip: if the effort drops to/below -50% while in Control mode (the vacuum valve is doing more than half the work), the app automatically switches the device to Measure mode and alerts the operator — a fail-safe against continuing to pull towards -100%
  • Max Safe Pressure: an optional, editable ceiling on the manual target field (and +/- step, and Scheduled Control items), pre-filled from the instrument's control-sensor full-scale (:INST:SENS1:FULL?) on connect but meant to be tightened per-experiment (e.g. to the gasket/diamond's actual safe limit) — not persisted across sessions, since the right value depends on what's currently mounted
  • Live pressure chart with configurable time window
  • CSV data logging
  • Scheduled control: build a sequence of pressure steps and waits, save/load as JSON, and run with live plot and automatic logging
  • HTTP API (standalone mode only): control and monitor the device from another process on the same machine, or another machine on the LAN — see API below

Main window

GUI Main Window

Scheduled control window

GUI scheduled control window

Requirements

  • Python 3.11+
  • PyQt6
  • pyqtgraph
  • pyserial

Install dependencies:

pip install PyQt6 pyqtgraph pyserial

Usage

app.py is the only supported standalone entry point:

python app.py

This works whether this repository (PaceMaker) is cloned on its own, or checked out as the apps/PACE5000/ submodule inside a larger project (e.g. bl18c_controller, run as python apps/PACE5000/app.py from that project's root) — app.py registers this directory as its own private package at runtime rather than assuming any particular parent directory structure, so pace5000_app.py, pace5000_ui_main.py, pace5000_backend.py, and pace5000_api.py always import each other consistently either way.

Connection settings (IP, port, COM port, baud rate) are saved automatically to pace5000_settings.json. The last-used log save directory is also persisted in pace5000_settings.json and restored as the default on next launch.

API

Only available when running app.py standalone (not when this app is embedded in another launcher). Open API → Configure and start API from the menu bar once connected and enable it there, or auto-start it with --api on the command line:

python app.py --api --api-host 0.0.0.0 --api-port 8765 --api-key <key>

Authentication: binding to 127.0.0.1 (the default) requires no API key — only processes on the same machine can reach it. Binding to any other host (e.g. 0.0.0.0 or a specific LAN IP, to allow other machines on the network to reach it) requires an API key, sent as the X-API-Key header on every request. Generate one from the UI ("Regenerate") or pass --api-key; it is persisted in pace5000_settings.json so it stays stable across restarts.

All endpoints are under /api/v1. Request/response bodies are JSON.

MethodPathAuthDescription
GET/api/v1/healthno{"ok": true} — liveness check
GET/api/v1/statusyesCurrent pressure, target, slew rate, control mode, source pressures, controller effort (effort_percent). All pressure/rate fields are always reported in MPa (_mpa suffix) regardless of the unit currently selected in the Manual Control tab (see implementation note below).
POST/api/v1/pressureyesBody {"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min", "token": "..."}. Sets the slew rate (verified) then the setpoint (verified); returns immediately once the device readback confirms the setpoint — does not wait for the pressure to arrive. unit is "MPa" or "Bar"; rate_unit is "MPa/min", "Bar/min", "MPa/sec", or "Bar/sec". token is only required if someone currently holds the lease (see below) — omit it otherwise. 403 if a lease is held and token doesn't match it. 409 if the target exceeds the +ve source pressure, the device's slew rate can't be verified, or the setpoint readback doesn't confirm what was sent (e.g. silently clamped by the device). A 200 response means the device has genuinely accepted both values, not just that the write didn't error.
POST/api/v1/control_modeyesBody {"enabled": true, "token": "..."} — toggle Control/Measure mode. Verified via :OUTP:STAT? readback before responding; 403 if a lease is held and token doesn't match it; 409 if the device doesn't confirm the requested mode after a few retries.
GET/api/v1/pressure/waityesQuery tol, unit, timeout_s (capped at 300 s, must be at least Pace5000Backend.DEFAULT_STABILITY_DWELL_S s or 400). Blocks until the measured pressure has stayed within tol of the current target continuously for DEFAULT_STABILITY_DWELL_S seconds, or returns 408 on timeout. For waits longer than a few tens of seconds, prefer polling /status yourself instead of holding this connection open.
GET/api/v1/leaseyes{"held": bool, "owner": str|null, "expires_in_s": float|null} — current lease status.
POST/api/v1/lease/acquireyesBody {"owner": "my-script", "ttl_s": 60} (ttl_s optional, default 60, capped at 300). Claims exclusive write access to /pressure and /control_mode. 200 {"token": "..."} on success; 409 if someone else already holds it.
POST/api/v1/lease/renewyesBody {"token": "...", "ttl_s": 60}. Extends an unexpired lease you hold before it times out. 409 if the token doesn't match a currently-held lease.
POST/api/v1/lease/releaseyesBody {"token": "..."}. Releases the lease early. 409 if the token doesn't match a currently-held lease.

Example:

curl -H "X-API-Key: $KEY" -X POST http://127.0.0.1:8765/api/v1/pressure \
-H "Content-Type: application/json" \
-d '{"pressure": 2.0, "unit": "MPa", "rate": 0.2, "rate_unit": "MPa/min"}'
curl -H "X-API-Key: $KEY" http://127.0.0.1:8765/api/v1/status

Implementation note: pressure setting/waiting logic (slew-rate-before-setpoint ordering with read-back verification, and target-reached polling) lives in a single place — Pace5000Backend.set_pressure_with_ramp() / wait_for_pressure() — shared by this API, the manual control tab and Scheduled Control feature in this app, and the external bl18c_controller experimental scheduler that can embed this app. Sending the setpoint before the slew rate is confirmed risks the device approaching the new setpoint at whatever rate was previously in effect. Both the slew rate and the setpoint are read back and verified before set_pressure_with_ramp() returns — a caller (in particular an HTTP client, whose response is the only signal it gets) must not be told the call succeeded for a value the device silently rejected or clamped. set_pressure_with_ramp() takes a unit ("MPa" or "Bar") that determines the device's active :UNIT:PRES for that call — the API/Scheduled Control/exp_scheduler always convert to MPa first, while the manual tab passes the operator's currently selected display unit directly (via Pace5000Backend.set_active_pressure_unit()), since it keeps the device's active unit in sync with that selection.

Dwell-based progress in wait_for_pressure(): the "reached" decision is still made by the device's own :SENS:PRES:INL? flag (continuously in-band for DEFAULT_STABILITY_DWELL_S, see the class-level comment on that constant) — but callers that want to show live progress (Scheduled Control, exp_scheduler) need more than a bare current-vs-target readout, since sitting inside the arrival band isn't "reached" until the dwell time has elapsed. wait_for_pressure()'s on_update callback is therefore called on every poll as on_update(current_mpa, target_mpa, band_elapsed_s, dwell_s), where band_elapsed_s is a client-side, best-effort clock (reset to 0 the instant a sample falls outside tol_mpa, capped at dwell_s) — UI-only, not the authoritative reached signal.

Unit handling for /status: the device's :UNIT:PRES is one shared piece of state — whatever it's currently set to affects every pressure/rate reading, not just the ones a given caller cares about. Rather than forcing :UNIT:PRES MPA on every /status poll (which would fight the manual tab whenever the operator has it set to Bar, and would add two extra round trips per poll), Pace5000Backend tracks the unit last pushed to the device (_active_pressure_unit, kept in sync by every call site that writes :UNIT:PRES) and get_status_mpa() converts the raw readings to MPa client-side using that tracked unit. This is why any code that changes the device's active pressure unit must go through set_active_pressure_unit() rather than writing :UNIT:PRES directly — a direct write would desync the tracked unit and cause /status to report incorrect MPa values.

Owner lease: the API key alone doesn't stop two authenticated clients from racing each other on /pressure / /control_mode — it only proves each one is allowed to talk to the server. /api/v1/lease/* adds a single, server-wide, advisory lease on top: acquire it, get back a token, and include that token in every subsequent /pressure / /control_mode body. Nobody has to use it — a server with no lease held behaves exactly as before (fully backward compatible for the common single-operator case); it only starts rejecting other clients' writes (403) once someone has actually claimed exclusivity. A lease expires on its own after ttl_s (renew with /lease/renew before that if a long-running scan needs to hold it longer) so a client that crashes or loses its connection can't lock the API out indefinitely. This intentionally does not cover the embedded, non-HTTP usage of Pace5000Backend — the manual control tab, Scheduled Control, and the bl18c_controller experimental scheduler (apps/exp_scheduler, which imports Pace5000Backend directly and never goes through this HTTP layer) are entirely outside the lease's reach. Extending exclusivity to that path would need the lease state to live on Pace5000Backend itself rather than on the HTTP server, plus a decision on whether in-process callers should be required to acquire it too — deliberately left as a separate follow-up rather than folded into this HTTP-only version.

Developer

Hiroki Kobayashi (https://orcid.org/0000-0002-3682-7558)

About

Python-based GUI application for controlling Druck PACE5000

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages