diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..e01b646
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,65 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Build & Run Commands
+- Create environment: `mamba create -n openhsi python==3.10 openhsi flask`
+- Activate environment: `mamba activate openhsi`
+- Run server: `python server.py`
+- Run as service: `sudo systemctl start openhsi-flask.service`
+- Service management: `sudo systemctl {start|stop|restart|status} openhsi-flask.service`
+- View logs: `sudo journalctl -u openhsi-flask.service -f`
+
+## Code Style Guidelines
+- Indentation: 4 spaces
+- Line length: ~88 characters (Black default)
+- Imports: Group by source (stdlib, third-party, local); use parenthesized imports
+- Naming: Classes: PascalCase, Functions/vars: snake_case, Constants: UPPER_SNAKE_CASE
+- Error handling: Use try/except with specific exceptions, log errors with app.logger
+- Types: Type annotations not currently used
+- Documentation: Docstrings and Flask-RestX for API endpoints
+- API patterns: Use Flask-RestX models for validation and documentation
+
+## Architecture Overview
+
+### Core Application Structure
+- **Single-file Flask application** (`server.py`) with Flask-RestX for API documentation
+- **Threaded camera operations** with global state management using thread locks
+- **Real-time status polling** architecture for capture progress monitoring
+- **Bootstrap-based responsive frontend** with tabbed interface
+
+### Camera Integration Patterns
+- **Custom OpenHSI camera wrapper** extending `FlirCamera` with progress callbacks
+- **Thread-safe capture operations** using `capture_lock` and `capture_status` globals
+- **Dual-tier settings system**: Basic settings (exposure, lines, processing) and advanced settings (binning, windowing, etc.)
+- **Calibration file integration**: Hardcoded paths to `.json` settings and `.nc` calibration files
+
+### API Architecture
+- **RESTful endpoints** grouped by functionality (camera, file management, system)
+- **Flask-RestX models** for request/response validation and auto-generated Swagger docs
+- **Secure file operations** restricted to `/data` directory with path traversal protection
+- **System time management** with sudo permission handling
+
+### Frontend Communication
+- **AJAX polling pattern** (500ms intervals) for real-time status updates
+- **Tabbed interface** with Bootstrap for modular feature organization
+- **Progress monitoring** with elapsed time and capture rate display
+- **File browser integration** with download, view, and delete capabilities
+
+### Key Global Variables
+- `capture_lock`: Threading lock for camera operations
+- `capture_status`: Dictionary tracking capture state, progress, and timing
+- `log_messages`: Centralized logging system with timestamps and severity levels
+- `current_camera`: Global camera instance for thread-safe operations
+
+### File System Integration
+- **Secure file browsing** within `/data` directory only
+- **Multiple file operations**: view, download, delete files and empty folders
+- **Image display options**: histogram equalization and contrast adjustment
+- **Metadata extraction** for file listings
+
+## Deployment Configuration
+- **Systemd service**: Pre-configured in `assets/openhsi-flask.service`
+- **Nginx reverse proxy**: Configuration in `assets/openhsi.ngnix`
+- **Time permissions**: `setup_time_permissions.sh` for sudo date command access
+- **Production settings**: Environment variables and user permissions for `openhsi` user
\ No newline at end of file
diff --git a/README.md b/README.md
index 9b0278e..2a83140 100644
--- a/README.md
+++ b/README.md
@@ -2,4 +2,8 @@
An example simple webinterface to drive an OpenHSI camera.
-
\ No newline at end of file
+
+
+## Setup
+
+For detailed installation and setup instructions, see: [Setup Guide](docs/setup.md)
\ No newline at end of file
diff --git a/assets/logo.png b/assets/logo.png
new file mode 100644
index 0000000..dfd63ba
Binary files /dev/null and b/assets/logo.png differ
diff --git a/assets/openhsi-flask.service b/assets/openhsi-flask.service
new file mode 100644
index 0000000..7a9df72
--- /dev/null
+++ b/assets/openhsi-flask.service
@@ -0,0 +1,13 @@
+[Unit]
+Description=OpenHSI Flask Web Server
+After=network.target
+
+[Service]
+User=openhsi
+WorkingDirectory=/home/openhsi/orlar/simple-web-controller
+ExecStart=/home/openhsi/miniforge3/envs/openhsi/bin/python /home/openhsi/orlar/simple-web-controller/server.py
+Restart=always
+Environment=FLASK_ENV=production
+
+[Install]
+WantedBy=multi-user.target
diff --git a/assets/openhsi.ngnix b/assets/openhsi.ngnix
new file mode 100644
index 0000000..b2393a9
--- /dev/null
+++ b/assets/openhsi.ngnix
@@ -0,0 +1,12 @@
+server {
+ listen 80;
+ server_name _; # Replace with your domain or IP address
+
+ location / {
+ proxy_pass http://127.0.0.1:5000;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 0000000..f16aca5
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+ My New API
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/setup.md b/docs/setup.md
new file mode 100644
index 0000000..ad93b31
--- /dev/null
+++ b/docs/setup.md
@@ -0,0 +1,219 @@
+![[assets/logo.png]]
+
+## Install dependancies
+
+### Miniforge
+Download and install miniforge.
+`wget "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"`
+`bash Miniforge3-$(uname)-$(uname -m).sh`
+
+### Openhsi env
+make python env and install openhsi+depenacies (match python version to that required by FLIR/Spinaker)
+`mamba create -n openhsi python==3.10 openhsi flask`
+`mamba activate openhsi`
+
+The rest assume you have openhsi env activated etc.
+
+### # Spinnaker SDK (FLIR camera, see openhsi docs for other cameras)
+Download SDK.
+https://www.teledynevisionsolutions.com/products/spinnaker-sdk/?model=Spinnaker%20SDK&vertical=machine%20vision&segment=iis
+
+#### Spinnaker SDK 4.2.0.46 for Ubuntu 22.04 (January 10, 2025)
+https://flir.netx.net/file/asset/68772/original/attachment - 64-bit ARM SDK
+https://flir.netx.net/file/asset/68774/original/attachment - Python 3.10 aarch64
+
+`wget -O spinnaker_python-4.2.0.46-cp310-cp310-linux_aarch64-22.04.tar.gz https://flir.netx.net/file/asset/68774/original/attachment
+`wget -O spinnaker-4.2.0.46-arm64-22.04-pkg.tar.gz https://flir.netx.net/file/asset/68772/original/attachment`
+
+`tar -xvzf spinnaker_python-4.2.0.46-cp310-cp310-linux_aarch64-22.04.tar.gz`
+`tar -xvzf spinnaker-4.2.0.46-arm64-22.04-pkg.tar.gz`
+
+`sudo apt-get install libusb-1.0-0 qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools`
+`sudo sh install_spinnaker_arm.sh`
+
+Answer yes to all quesitons. At "Adding new members to usergroup flirimaging", and any users on system that need to access camera., eg. *openhsi* user.
+
+`pip install spinnaker_python-4.2.0.46-cp310-cp310-linux_aarch64.whl --no-deps`
+`pip install simple-pyspin --no-deps`
+
+## Install OpenHSI Orlar code
+Clone or download this repo.
+
+
+#### Setup Systemd Auto Start
+
+##### Raspberry Pi Permissions Note
+By default on Raspberry Pi, the original user (typically `pi`) has sudo NOPASSWD privileges configured in `/etc/sudoers.d/010_pi-nopasswd`. This means the default user can run sudo commands without entering a password.
+
+##### Service Installation
+From the repo folder:
+
+```bash
+# Copy the service file to systemd directory
+sudo cp assets/openhsi-flask.service /etc/systemd/system/openhsi-flask.service
+
+# Edit the service file to match your paths (if different from default)
+sudo nano /etc/systemd/system/openhsi-flask.service
+
+# Reload systemd to recognize the new service
+sudo systemctl daemon-reload
+
+# Enable the service to start on boot
+sudo systemctl enable openhsi-flask.service
+
+# Start the service now
+sudo systemctl start openhsi-flask.service
+
+# Check service status
+sudo systemctl status openhsi-flask.service
+```
+
+##### Service Management Commands
+```bash
+# Start the service
+sudo systemctl start openhsi-flask.service
+
+# Stop the service
+sudo systemctl stop openhsi-flask.service
+
+# Restart the service
+sudo systemctl restart openhsi-flask.service
+
+# Check service status and logs
+sudo systemctl status openhsi-flask.service
+sudo journalctl -u openhsi-flask.service -f # Follow logs in real-time
+sudo journalctl -u openhsi-flask.service --since today # Today's logs
+```
+
+##### Time Permissions Setup
+The OpenHSI application may need to set the system time for synchronization purposes. The included `setup_time_permissions.sh` script configures the necessary permissions:
+
+**Note for Raspberry Pi OS:** The default Raspberry Pi OS does not require this permissions script. The default user (typically `pi`) already has passwordless sudo access for all commands via `/etc/sudoers.d/010_pi-nopasswd`. This script is useful for:
+- Non-Raspberry Pi systems
+- Custom user accounts without full sudo access
+- Production deployments where you want to limit sudo permissions to only the `date` command
+
+```bash
+# Make the script executable
+chmod +x setup_time_permissions.sh
+
+# Run as root to set up time permissions for the openhsi user
+sudo ./setup_time_permissions.sh
+
+# Or specify a different user
+sudo ./setup_time_permissions.sh myuser
+```
+
+**What the script does:**
+1. **Creates a sudoers rule** in `/etc/sudoers.d/openhsi-time`
+2. **Allows the specified user** (default: `openhsi`) to run `sudo date` without password
+3. **Sets proper permissions** (440) on the sudoers file for security
+4. **Validates syntax** using `visudo -c` to prevent system lockout
+
+**Security Note:** This only grants permission to run the `date` command with sudo, not full sudo access.
+
+**Usage after setup:**
+```bash
+# The openhsi user can now set system time without password prompt
+sudo date -s '2024-12-25 10:30:00'
+```
+
+## Updating the System
+
+### Update OpenHSI Package
+To update the OpenHSI package to the latest version:
+
+```bash
+# Activate the openhsi environment
+mamba activate openhsi
+
+# Update OpenHSI package
+mamba update openhsi
+
+# Or update all packages in the environment
+mamba update --all
+```
+
+### Update Repository Code
+
+#### Update to Latest Development Version
+```bash
+# Navigate to the repository directory
+cd /path/to/simple-web-controller
+
+# Pull latest changes from the dev branch (active development)
+git pull origin dev
+
+# Or pull from main branch (stable releases)
+git pull origin main
+
+# Restart the service to apply changes
+sudo systemctl restart openhsi-flask.service
+```
+
+#### Update to Specific Release
+```bash
+# Navigate to the repository directory
+cd /path/to/simple-web-controller
+
+# Fetch all tags and releases
+git fetch --tags
+
+# List available release tags
+git tag -l
+
+# Checkout a specific release (replace v1.2.3 with desired version)
+git checkout v1.2.3
+
+# Restart the service to apply changes
+sudo systemctl restart openhsi-flask.service
+```
+
+#### Check Current Version
+```bash
+# Check current git commit/tag
+git describe --tags --always
+
+# Check current branch
+git branch --show-current
+
+# View recent commits
+git log --oneline -5
+```
+
+### Update Workflow
+1. Stop the service: `sudo systemctl stop openhsi-flask.service`
+2. Update OpenHSI package (if needed): `mamba update openhsi`
+3. Update repository code: `git pull` or `git checkout `
+4. Start the service: `sudo systemctl start openhsi-flask.service`
+5. Check service status: `sudo systemctl status openhsi-flask.service`
+
+
+#### ngnix port 80 proxy
+Setup ngnix proxy so interface can accessed via browser without port.
+
+`sudo apt update`
+`sudo apt install nginx`
+
+`sudo rm /etc/nginx/sites-enabled/default`
+`sudp cp assets//openhsi.ngnix /etc/nginx/sites-available/openhsi`
+`sudo ln -s /etc/nginx/sites-available/openhsi /etc/nginx/sites-enabled/`
+
+`sudo nginx -t`
+`sudo systemctl reload nginx`
+
+
+## Setup Tailscale (FOR REMOTE ACCESS)
+Depending on use case and deployment, it may be sensible to setup a remote access system, such as tailscale.com
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/swagger.json b/docs/swagger.json
new file mode 100644
index 0000000..c7ccaaa
--- /dev/null
+++ b/docs/swagger.json
@@ -0,0 +1,503 @@
+{
+ "swagger": "2.0",
+ "basePath": "/api",
+ "paths": {
+ "/capture": {
+ "post": {
+ "responses": {
+ "200": {
+ "description": "Capture started or already in progress"
+ }
+ },
+ "summary": "Start the image capture process",
+ "operationId": "post_capture",
+ "tags": [
+ "default"
+ ]
+ }
+ },
+ "/delete/{filename}": {
+ "parameters": [
+ {
+ "name": "filename",
+ "in": "path",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "delete": {
+ "responses": {
+ "500": {
+ "description": "Error occurred while deleting file"
+ },
+ "404": {
+ "description": "File not found"
+ },
+ "403": {
+ "description": "Forbidden - Cannot delete outside data directory"
+ },
+ "200": {
+ "description": "File deleted successfully"
+ }
+ },
+ "summary": "Delete a file from the data directory",
+ "operationId": "delete_delete_file",
+ "parameters": [
+ {
+ "name": "filename",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "The file path relative to the data directory"
+ }
+ ],
+ "tags": [
+ "default"
+ ]
+ }
+ },
+ "/download/{filename}": {
+ "parameters": [
+ {
+ "name": "filename",
+ "in": "path",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "get": {
+ "responses": {
+ "404": {
+ "description": "File not found"
+ },
+ "200": {
+ "description": "File sent as attachment"
+ }
+ },
+ "summary": "Download a file from the data directory",
+ "operationId": "get_download",
+ "parameters": [
+ {
+ "name": "filename",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "The file path relative to the data directory"
+ }
+ ],
+ "tags": [
+ "default"
+ ]
+ }
+ },
+ "/file_list": {
+ "get": {
+ "responses": {
+ "404": {
+ "description": "Directory not found"
+ },
+ "403": {
+ "description": "Forbidden - Cannot access directory outside data directory"
+ },
+ "200": {
+ "description": "File list retrieved successfully"
+ }
+ },
+ "summary": "Get a list of all files in the specified directory",
+ "operationId": "get_file_list",
+ "parameters": [
+ {
+ "required": false,
+ "in": "query",
+ "description": "The folder path to list (relative to data directory)",
+ "name": "folder",
+ "type": "string"
+ }
+ ],
+ "tags": [
+ "default"
+ ]
+ }
+ },
+ "/logs": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "Log messages retrieved successfully"
+ }
+ },
+ "summary": "Retrieve the log messages",
+ "operationId": "get_log_messages",
+ "tags": [
+ "default"
+ ]
+ },
+ "delete": {
+ "responses": {
+ "200": {
+ "description": "Log messages cleared successfully"
+ }
+ },
+ "summary": "Clear the log messages",
+ "operationId": "delete_log_messages",
+ "tags": [
+ "default"
+ ]
+ }
+ },
+ "/save": {
+ "post": {
+ "responses": {
+ "500": {
+ "description": "Error occurred while saving files"
+ },
+ "200": {
+ "description": "Files saved successfully"
+ }
+ },
+ "summary": "Save the captured files to a specified directory",
+ "operationId": "post_save_files",
+ "parameters": [
+ {
+ "name": "payload",
+ "required": true,
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/Save"
+ }
+ }
+ ],
+ "tags": [
+ "default"
+ ]
+ }
+ },
+ "/show": {
+ "get": {
+ "responses": {
+ "204": {
+ "description": "No Content \u2013 capture not finished or image generation error"
+ },
+ "200": {
+ "description": "Image retrieved successfully"
+ }
+ },
+ "summary": "Retrieve the captured image as a PNG file with display options",
+ "operationId": "get_show_image",
+ "parameters": [
+ {
+ "type": "integer",
+ "in": "query",
+ "description": "Contrast stretch percentage",
+ "name": "stretch"
+ },
+ {
+ "type": "string",
+ "in": "query",
+ "description": "Band to display (rgb, red, green, blue, nir)",
+ "name": "band"
+ },
+ {
+ "type": "boolean",
+ "in": "query",
+ "description": "Apply robust contrast stretching",
+ "name": "robust"
+ },
+ {
+ "type": "boolean",
+ "in": "query",
+ "description": "Apply histogram equalization",
+ "name": "hist_eq"
+ }
+ ],
+ "tags": [
+ "default"
+ ]
+ }
+ },
+ "/status": {
+ "get": {
+ "responses": {
+ "200": {
+ "description": "Status retrieved successfully"
+ }
+ },
+ "summary": "Retrieve the current capture status along with progress details",
+ "operationId": "get_status",
+ "tags": [
+ "default"
+ ]
+ }
+ },
+ "/update_settings": {
+ "post": {
+ "responses": {
+ "500": {
+ "description": "Internal error while updating settings"
+ },
+ "400": {
+ "description": "Invalid input"
+ },
+ "200": {
+ "description": "Settings updated successfully"
+ }
+ },
+ "summary": "Update camera settings (both basic and advanced)",
+ "description": "This endpoint handles both the basic settings (n_lines, exposure_ms, processing_lvl)\nand the advanced detailed settings for the camera.\n\nAdvanced settings include:\n- row_slice: Range of rows to read from detector [start, end]\n- resolution: Image resolution [height, width]\n- fwhm_nm: Full Width at Half Maximum (spectral resolution) in nanometers\n- exposure_ms: Exposure time in milliseconds\n- luminance: Luminance value for calibration\n- binxy: Binning factors [x, y]\n- win_offset: Window offset [x, y]\n- win_resolution: Window resolution [width, height]\n- pixel_format: Pixel format (Mono8, Mono12, or Mono16)",
+ "operationId": "post_update_settings",
+ "parameters": [
+ {
+ "name": "payload",
+ "required": true,
+ "in": "body",
+ "schema": {
+ "$ref": "#/definitions/FullSettings"
+ }
+ },
+ {
+ "description": "Number of scan lines to capture",
+ "name": "n_lines",
+ "type": "string",
+ "in": "query"
+ },
+ {
+ "description": "Exposure time in milliseconds",
+ "name": "exposure_ms",
+ "type": "string",
+ "in": "query"
+ },
+ {
+ "description": "Processing level (-1 to 4)",
+ "name": "processing_lvl",
+ "type": "string",
+ "in": "query"
+ },
+ {
+ "description": "Range of rows to read from detector [start, end]",
+ "name": "row_slice",
+ "type": "string",
+ "in": "query"
+ },
+ {
+ "description": "Image resolution [height, width]",
+ "name": "resolution",
+ "type": "string",
+ "in": "query"
+ },
+ {
+ "description": "Full Width at Half Maximum (spectral resolution) in nanometers",
+ "name": "fwhm_nm",
+ "type": "string",
+ "in": "query"
+ },
+ {
+ "description": "Luminance value for calibration",
+ "name": "luminance",
+ "type": "string",
+ "in": "query"
+ },
+ {
+ "description": "Binning factors [x, y]",
+ "name": "binxy",
+ "type": "string",
+ "in": "query"
+ },
+ {
+ "description": "Window offset [x, y]",
+ "name": "win_offset",
+ "type": "string",
+ "in": "query"
+ },
+ {
+ "description": "Window resolution [width, height]",
+ "name": "win_resolution",
+ "type": "string",
+ "in": "query"
+ },
+ {
+ "description": "Pixel format (Mono8, Mono12, or Mono16)",
+ "name": "pixel_format",
+ "type": "string",
+ "in": "query"
+ }
+ ],
+ "tags": [
+ "default"
+ ]
+ }
+ },
+ "/view/{filename}": {
+ "parameters": [
+ {
+ "name": "filename",
+ "in": "path",
+ "required": true,
+ "type": "string"
+ }
+ ],
+ "get": {
+ "responses": {
+ "404": {
+ "description": "File not found"
+ },
+ "200": {
+ "description": "File sent for viewing"
+ }
+ },
+ "summary": "View a file (especially images) in the browser without downloading",
+ "operationId": "get_view_file",
+ "parameters": [
+ {
+ "name": "filename",
+ "in": "query",
+ "required": true,
+ "type": "string",
+ "description": "The file path relative to the data directory"
+ }
+ ],
+ "tags": [
+ "default"
+ ]
+ }
+ }
+ },
+ "info": {
+ "title": "OpenHSI Capture API",
+ "version": "1.1",
+ "description": "API for managing OpenHSI capture and file operations"
+ },
+ "produces": [
+ "application/json"
+ ],
+ "consumes": [
+ "application/json"
+ ],
+ "tags": [
+ {
+ "name": "default",
+ "description": "Default namespace"
+ }
+ ],
+ "definitions": {
+ "FullSettings": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/Settings"
+ },
+ {
+ "properties": {
+ "row_slice": {
+ "type": "array",
+ "description": "Range of rows to read from detector [start, end]",
+ "example": [
+ 8,
+ 913
+ ],
+ "items": {
+ "type": "integer"
+ }
+ },
+ "resolution": {
+ "type": "array",
+ "description": "Image resolution [height, width]",
+ "example": [
+ 924,
+ 1240
+ ],
+ "items": {
+ "type": "integer"
+ }
+ },
+ "fwhm_nm": {
+ "type": "number",
+ "description": "Full Width at Half Maximum (spectral resolution) in nanometers",
+ "example": 4.0
+ },
+ "luminance": {
+ "type": "number",
+ "description": "Luminance value for calibration",
+ "example": 10000
+ },
+ "binxy": {
+ "type": "array",
+ "description": "Binning factors [x, y]",
+ "example": [
+ 1,
+ 1
+ ],
+ "items": {
+ "type": "integer"
+ }
+ },
+ "win_offset": {
+ "type": "array",
+ "description": "Window offset [x, y]",
+ "example": [
+ 96,
+ 200
+ ],
+ "items": {
+ "type": "integer"
+ }
+ },
+ "win_resolution": {
+ "type": "array",
+ "description": "Window resolution [width, height]",
+ "example": [
+ 924,
+ 1240
+ ],
+ "items": {
+ "type": "integer"
+ }
+ },
+ "pixel_format": {
+ "type": "string",
+ "description": "Pixel format (Mono8, Mono12, or Mono16)",
+ "example": "Mono8"
+ }
+ },
+ "type": "object"
+ }
+ ]
+ },
+ "Settings": {
+ "properties": {
+ "n_lines": {
+ "type": "integer",
+ "description": "Number of lines",
+ "example": 512
+ },
+ "exposure_ms": {
+ "type": "number",
+ "description": "Exposure time in milliseconds",
+ "example": 10.0
+ },
+ "processing_lvl": {
+ "type": "integer",
+ "description": "Processing level",
+ "example": -1
+ }
+ },
+ "type": "object"
+ },
+ "Save": {
+ "properties": {
+ "save_dir": {
+ "type": "string",
+ "description": "Directory where files will be saved",
+ "example": "/data"
+ }
+ },
+ "type": "object"
+ }
+ },
+ "responses": {
+ "ParseError": {
+ "description": "When a mask can't be parsed"
+ },
+ "MaskError": {
+ "description": "When any error occurs on mask"
+ }
+ }
+}
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..e27722e
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,41 @@
+[build-system]
+requires = ["setuptools>=45", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "openhsi-web-controller"
+version = "1.2.0"
+description = "Flask-based web controller for OpenHSI cameras"
+readme = "README.md"
+requires-python = ">=3.8"
+authors = [
+ {name = "OpenHSI Team"}
+]
+keywords = ["openhsi", "camera", "web", "controller", "flask"]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Science/Research",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+]
+dependencies = [
+ "flask",
+ "flask-restx",
+ "openhsi",
+ "holoviews",
+ "matplotlib",
+ "tqdm"
+]
+
+[project.urls]
+Homepage = "https://github.com/openhsi/simple-web-controller"
+Repository = "https://github.com/openhsi/simple-web-controller"
+Documentation = "https://github.com/openhsi/simple-web-controller#readme"
+
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["*"]
+exclude = ["tests*"]
diff --git a/server.py b/server.py
index 8919f25..428c59b 100644
--- a/server.py
+++ b/server.py
@@ -1,172 +1,644 @@
-from flask import Flask, request, jsonify, render_template, send_file, send_from_directory, abort
+from flask import (
+ Flask,
+ request,
+ jsonify,
+ render_template,
+ send_file,
+ send_from_directory,
+ abort,
+ Blueprint,
+)
+from flask_restx import Api, Resource, fields
import threading
import os
+import time
from io import BytesIO
import tempfile
import holoviews as hv
+from tqdm import tqdm
import matplotlib
-matplotlib.use('Agg')
+import subprocess
+import datetime
+import os
+import re
+def get_version():
+ """Get version from pyproject.toml"""
+ try:
+ with open('pyproject.toml', 'r') as f:
+ content = f.read()
+ match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
+ return match.group(1) if match else "1.1.1"
+ except:
+ return "1.1.1"
+
+# Application version
+__version__ = get_version()
+
+matplotlib.use("Agg")
+
+from openhsi.cameras import FlirCamera as openhsiCameraOrig
+
+# openhsi calibration settings
+# json_path = "/home/openhsi/UNE/cals/OpenHSI-SAIL-UNE-01/OpenHSI-SAIL-UNE-01_settings_Mono8_bin1.json"
+# cal_path = "/home/openhsi/UNE/cals/OpenHSI-SAIL-UNE-01/OpenHSI-SAIL-UNE-01_calibration_Mono8_bin1.nc"
+json_path = "/home/openhsi/orlar/cals/OpenHSI-SAIL-orlar-01/OpenHSI-SAIL-orlar-01_settings_Mono8_bin1.json"
+cal_path = "/home/openhsi/orlar/cals/OpenHSI-SAIL-orlar-01/OpenHSI-SAIL-orlar-01_calibration_Mono8_bin1.nc"
+
+
+# reimplemnted openhsi capture to allow capture progress feedback.
+class openhsiCamera(openhsiCameraOrig):
+ def __init__(self, **kwargs):
+ super().__init__(**kwargs)
-# from openhsi.cameras import FlirCamera as openhsiCamera
-from openhsi.capture import SimulatedCamera as openhsiCamera
+ def collect(self, progress_callback=None):
+ self.start_cam()
+ pbar = tqdm(range(self.n_lines))
+ for _ in pbar:
+ self.put(self.get_img())
+ if callable(getattr(self, "get_temp", None)):
+ self.cam_temperatures.put(self.get_temp())
+ # If a progress_callback is provided, extract the progress data from pbar.
+ if progress_callback:
+ # pbar.format_dict returns a dictionary with useful keys
+ # such as 'n', 'total', 'elapsed', and 'eta'.
+ progress_callback(pbar.format_dict)
+ self.stop_cam()
+
+
+# Initialize the camera at startup with explicit parameters.
+cam = openhsiCamera(
+ n_lines=512,
+ exposure_ms=10,
+ processing_lvl=-1,
+ json_path=json_path,
+ cal_path=cal_path,
+)
app = Flask(__name__)
+# Create a blueprint for the API with a URL prefix (e.g. '/api')
+api_bp = Blueprint("api", __name__, url_prefix="/api")
+api = Api(
+ api_bp,
+ version=get_version(),
+ title="OpenHSI Capture API",
+ description="API for managing OpenHSI capture and file operations",
+ doc="/apidocs",
+
+) # Swagger UI will be at /api/apidocs
+
+app.config['SWAGGER_UI_DOC_EXPANSION'] = 'list'
+
+# Register the blueprint with the main Flask app.
+app.register_blueprint(api_bp)
+
+# Define models for the API requests.
+settings_model = api.model(
+ "Settings",
+ {
+ "n_lines": fields.Integer(
+ required=False, description="Number of lines", example=512
+ ),
+ "exposure_ms": fields.Float(
+ required=False, description="Exposure time in milliseconds", example=10.0
+ ),
+ "processing_lvl": fields.Integer(
+ required=False, description="Processing level", example=-1
+ ),
+ },
+)
+
+# Model for time setting
+time_model = api.model(
+ "TimeSettings",
+ {
+ "datetime": fields.String(
+ required=True,
+ description="Date and time in ISO format (YYYY-MM-DDTHH:MM:SS)",
+ example="2024-01-01T12:00:00"
+ ),
+ },
+)
+
+# Model for advanced camera settings
+advanced_settings_model = api.model(
+ "AdvancedSettings",
+ {
+ "row_slice": fields.List(
+ fields.Integer,
+ required=False,
+ description="Range of rows to read from detector [start, end]",
+ example=[8, 913],
+ ),
+ "resolution": fields.List(
+ fields.Integer,
+ required=False,
+ description="Image resolution [height, width]",
+ example=[924, 1240],
+ ),
+ "fwhm_nm": fields.Float(
+ required=False,
+ description="Full Width at Half Maximum (spectral resolution) in nanometers",
+ example=4.0,
+ ),
+ "luminance": fields.Float(
+ required=False, description="Luminance value for calibration", example=10000
+ ),
+ "binxy": fields.List(
+ fields.Integer,
+ required=False,
+ description="Binning factors [x, y]",
+ example=[1, 1],
+ ),
+ "win_offset": fields.List(
+ fields.Integer,
+ required=False,
+ description="Window offset [x, y]",
+ example=[96, 200],
+ ),
+ "win_resolution": fields.List(
+ fields.Integer,
+ required=False,
+ description="Window resolution [width, height]",
+ example=[924, 1240],
+ ),
+ "pixel_format": fields.String(
+ required=False,
+ description="Pixel format (Mono8, Mono12, or Mono16)",
+ example="Mono8",
+ ),
+ },
+)
+
+# Update the settings model to include advanced settings
+full_settings_model = api.inherit(
+ "FullSettings", settings_model, advanced_settings_model
+)
+
+save_model = api.model(
+ "Save",
+ {
+ "save_dir": fields.String(
+ required=False,
+ description="Directory where files will be saved",
+ example="/data",
+ )
+ },
+)
+
# Define the list of settings to show.
SETTING_KEYS = ["n_lines", "exposure_ms", "processing_lvl"]
# Allowed processing levels with updated descriptions.
PROCESSING_LVL_OPTIONS = {
-1: "-1 - do not apply any transforms (default)",
- 0: "0 - crop to useable sensor area",
- 1: "1 - crop + fast smile",
- 2: "2 - crop + fast smile + fast binning",
- 3: "3 - crop + fast smile + slow binning",
- 4: "4 - crop + fast smile + fast binning + conversion to radiance in units of uW/cm^2/sr/nm"
+ 0: "0 - crop to useable sensor area",
+ 1: "1 - crop + fast smile",
+ 2: "2 - crop + fast smile + fast binning",
+ 3: "3 - crop + fast smile + slow binning",
+ 4: "4 - crop + fast smile + fast binning + conversion to radiance in units of uW/cm^2/sr/nm",
}
-# Initialize the camera at startup with explicit parameters.
-cam = openhsiCamera(
- img_path='assets/great_hall_slide.png',
- n_lines=1024,
- exposure_ms=1,
- processing_lvl=-1,
- json_path="assets/cam_settings.json",
- cal_path="assets/cam_calibration.nc"
-)
+# Define detailed settings with types, descriptions, and validation
+DETAILED_SETTINGS = {
+ "row_slice": {
+ "type": "array_int",
+ "description": "Range of rows to read from detector [start, end]",
+ "min_value": 0,
+ "max_value": 1024,
+ "size": 2,
+ },
+ "resolution": {
+ "type": "array_int",
+ "description": "Image resolution [height, width]",
+ "min_value": 1,
+ "max_value": 2048,
+ "size": 2,
+ },
+ "fwhm_nm": {
+ "type": "float",
+ "description": "Full Width at Half Maximum (spectral resolution) in nanometers",
+ "min_value": 0.1,
+ "max_value": 100,
+ },
+ "exposure_ms": {
+ "type": "float",
+ "description": "Exposure time in milliseconds",
+ "min_value": 0.1,
+ "max_value": 1000,
+ },
+ "luminance": {
+ "type": "float",
+ "description": "Luminance value for calibration",
+ "min_value": 0,
+ "max_value": 100000,
+ },
+ "binxy": {
+ "type": "array_int",
+ "description": "Binning factors [x, y]",
+ "min_value": 1,
+ "max_value": 8,
+ "size": 2,
+ },
+ "win_offset": {
+ "type": "array_int",
+ "description": "Window offset [x, y]",
+ "min_value": 0,
+ "max_value": 2048,
+ "size": 2,
+ },
+ "win_resolution": {
+ "type": "array_int",
+ "description": "Window resolution [width, height]",
+ "min_value": 1,
+ "max_value": 2048,
+ "size": 2,
+ },
+ "pixel_format": {
+ "type": "select",
+ "description": "Pixel format",
+ "options": ["Mono8", "Mono12", "Mono16"],
+ },
+}
# Global flags and lock for capture status.
collection_running = False
capture_finished = False
collection_lock = threading.Lock()
+# Log messages storage
+log_messages = []
+log_lock = threading.Lock()
+
+
+def add_log_message(message, message_type="info"):
+ """Add a message to the log with timestamp and type."""
+ with log_lock:
+ timestamp = int(time.time() * 1000) # milliseconds since epoch
+ log_messages.append(
+ {
+ "timestamp": timestamp,
+ "time": time.strftime("%H:%M:%S"),
+ "message": message,
+ "type": message_type,
+ }
+ )
+ # Keep only the last 100 messages
+ if len(log_messages) > 100:
+ log_messages.pop(0)
+
+
def run_collection():
- global collection_running, capture_finished
+ global collection_running, capture_finished, capture_progress
with collection_lock:
collection_running = True
- capture_finished = False # Clear finished flag at start.
+ capture_finished = False
+ capture_progress = {}
try:
- # This call blocks until capture completes.
- cam.collect()
+ # Pass the update_progress callback, which now receives the tqdm progress dict.
+ add_log_message("Collection process started", "info")
+ cam.collect(progress_callback=update_progress)
+ add_log_message("Collection completed successfully", "success")
+ except Exception as e:
+ add_log_message(f"Error during collection: {str(e)}", "error")
+ app.logger.error(f"Collection error: {e}")
finally:
with collection_lock:
collection_running = False
- capture_finished = True # Set finished flag when done.
+ capture_finished = True
+
+
+# Global variable to store progress info.
+capture_progress = {}
+
+
+def update_progress(progress_info):
+ global capture_progress
+ # Extract desired values from progress_info.
+ current = progress_info.get("n", 0)
+ total = progress_info.get("total", 0)
+ elapsed = progress_info.get("elapsed", 0)
+ rate = progress_info.get("rate", 0)
+ percentage = (current / total) * 100 if total else 0
+ capture_progress = {
+ "current": current,
+ "total": total,
+ "elapsed": elapsed,
+ "rate": rate,
+ "percentage": percentage,
+ }
-@app.route('/')
+
+# -------------------------------------------------------------------------
+# Non-API route: Render the main index page with a settings form.
+@app.route("/")
def index():
# Generate form fields HTML from the settings.
form_fields = ""
- for key in ["n_lines", "exposure_ms", "processing_lvl"]:
+ for key in SETTING_KEYS:
if key == "processing_lvl":
current_value = cam.settings.get(key, "")
form_fields += f'