#!/usr/bin/env python3 # SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute # SPDX-License-Identifier: GPL-3.0-only """A stand-in for a DECTRIS detector's SIMPLON control interface, for running jfjoch_broker without a detector (demonstrations and tests). It answers the configuration and status requests the broker makes, accepts every command, and always reads back as idle. The images come from tools/jfjoch_stream2_replay, which plays the detector's stream2. fake_simplon.py --width 2068 --height 2164 --pixel-size 75e-6 [--port 80] The broker reaches SIMPLON on port 80, so this has to be able to bind it (e.g. run it as root in a container on the host network). """ import argparse import base64 import json from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer def main(): p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) p.add_argument("--width", type=int, required=True) p.add_argument("--height", type=int, required=True) p.add_argument("--pixel-size", type=float, required=True, help="metres") p.add_argument("--sensor-thickness", type=float, default=320e-6, help="metres") p.add_argument("--sensor-material", default="Si") p.add_argument("--bit-depth", type=int, default=32) p.add_argument("--description", default="Replayed detector") p.add_argument("--port", type=int, default=80) a = p.parse_args() mask = base64.b64encode(bytes(4 * a.width * a.height)).decode() config = { "description": {"value": a.description}, "detector_number": {"value": "replay-1"}, "x_pixel_size": {"value": a.pixel_size}, "y_pixel_size": {"value": a.pixel_size}, "sensor_material": {"value": a.sensor_material}, "sensor_thickness": {"value": a.sensor_thickness}, "bit_depth_image": {"value": a.bit_depth}, "x_pixels_in_detector": {"value": a.width}, "y_pixels_in_detector": {"value": a.height}, "frame_time": {"value": 0.01, "min": 0.0001}, "count_time": {"value": 0.01, "min": 0.0001}, "detector_readout_time": {"value": 0.0}, "threshold_energy": {"value": 4000.0, "min": 1000.0}, "photon_energy": {"value": 8000.0}, "nimages": {"value": 1}, "ntrigger": {"value": 1}, "pixel_mask": {"value": {"shape": [a.height, a.width], "type": "uint32", "data": mask}}, } status = {"state": {"value": "idle"}, "humidity": {"value": 0.0}, "temperature": {"value": 25.0}} class Handler(BaseHTTPRequestHandler): def reply(self, body): data = json.dumps(body).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Content-Length", str(len(data))) self.end_headers() self.wfile.write(data) def do_GET(self): parts = self.path.strip("/").split("/") # module/api/version/task/key key = parts[-1] if len(parts) >= 5 and parts[3] == "status": self.reply(status.get(key, {"value": 0})) else: self.reply(config.get(key, {"value": 0})) def do_POST(self): parts = self.path.strip("/").split("/") length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length) if length else b"" if len(parts) >= 5 and parts[3] == "config" and body: config.setdefault(parts[-1], {})["value"] = json.loads(body)["value"] self.reply([]) do_PUT = do_POST def log_message(self, fmt, *args): pass ThreadingHTTPServer(("0.0.0.0", a.port), Handler).serve_forever() if __name__ == "__main__": main()