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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,93 @@
# Automated Viscosity Measurements

This project automates viscosity measurements using a viscometer, CNC machine for sample handling, and ESP32 for pump control. It supports single RPM, dynamic, and bisection analysis modes.

## Project Structure

```
src/
├── hardware/
│ ├── viscometer/ # Viscometer protocol and client
│ ├── cnc/ # CNC controller
│ └── pump/ # ESP32 pump control
├── motion/ # Sample handling and wash sequences
├── analysis/ # Analysis methods (single, dynamic, bisection)
└── runner/ # Main entry point and bridge to 32-bit worker

config/
├── default.yaml # Main configuration (ports, modes, etc.)
└── locations.yaml # CNC sample and wash station positions

results/ # Output CSV files per sample
```

## Installation

### Prerequisites

- Python 3.8+ (64-bit for main application)
- Python 3.8+ (32-bit for viscometer worker subprocess)
- Serial ports for viscometer (COM6), CNC (COM5), ESP32 pump (COM4)

### Setup Virtual Environments

1. Create 64-bit venv:
```bash
python -m venv venv64
venv64\Scripts\activate
pip install -r requirements.txt
```

2. Create 32-bit venv (if using separate Python installation):
```bash
# Assuming python32.exe is available
python32 -m venv venv32
venv32\Scripts\activate
pip install -r requirements.txt
```

### Install Dependencies

```bash
pip install -r requirements.txt
```

## Configuration

Edit `config/default.yaml` to set:

- Serial ports and baud rates
- Analysis mode (`single`, `dynamic`, `bisection`)
- Sample rack and range
- Wash settings

Edit `config/locations.yaml` for CNC positions.

## Usage

1. Ensure hardware is connected and ports are correct.

2. Run the main application:
```bash
python src/runner/main.py
```

This will:
- Load configuration
- Initialize hardware
- Move to samples, perform analysis, save CSVs to `results/`

3. Results are saved as CSV files in `results/sample_XXX/` directories.

## Analysis Modes

- **Single**: Spin at fixed RPM, log torque/temperature over time
- **Dynamic**: Test multiple RPMs, record steady-state values
- **Bisection**: Find RPM for target torque using binary search

## Troubleshooting

- Ensure serial ports are not in use by other applications
- Check virtual mode in config for testing without hardware
- 32-bit worker requires DVT_COM.dll in `src/hardware/viscometer/`

2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pyserial>=3.5
pyyaml>=6.0
18 changes: 18 additions & 0 deletions visc_automated_workflow_V3/config/default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Default configuration for automated viscosity measurements
python32: ".\\.venv32\\Scripts\\python.exe"
visco_port: "COM6"
visco_baud: 115200
visco_timeout: 1.0
spindle_k: 992.47
analysis_mode: "single" # "single" | "dynamic" | "bisection"
sample_rack: "main_rack_A"
sample_range: [0]
enable_wash: false
esp32_port: "COM4"
esp32_baud: 9600
pump_virtual: true
pause_after_home: 0.2
pause_after_move: 0.1
cnc_port: "COM5"
cnc_baud: 115200
location_file: "config/locations.yaml"
Empty file.
Empty file.
71 changes: 71 additions & 0 deletions visc_automated_workflow_V3/src/analysis/bisection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import csv, time, pathlib
from hardware.viscometer.client import ViscometerClient

def run_bisection(results_dir: pathlib.Path, client: ViscometerClient):
TARGET_TORQUE_PCT = 50.0
TOL_PCT = 20.0
LOW_RPM = 0.5
HIGH_RPM = 30
MAX_ITERS = 20
SETTLE_SECONDS = 60.0
FINAL_HOLD_S = 60.0
INTER_PAUSE_SEC = 2.0
CSV_NAME = "bisection_analysis.csv"

out = results_dir / CSV_NAME
out.parent.mkdir(parents=True, exist_ok=True)

history = []
lo, hi = float(LOW_RPM), float(HIGH_RPM)
best_rpm, best_err = None, float("inf")

for _ in range(MAX_ITERS):
mid = (lo + hi) / 2.0
client.set_speed(mid)
time.sleep(SETTLE_SECONDS)
pkt = client.read_single(timeout=1.0)
client.stop()
time.sleep(INTER_PAUSE_SEC)

if not pkt or not pkt.get("torque_valid"):
# treat as unusable datapoint; shrink range slightly around mid and continue
lo = max(0.1, mid * 0.9)
hi = min(200.0, mid * 1.1)
continue

tq = float(pkt["torque_percent"])
err = abs(tq - TARGET_TORQUE_PCT)
history.append({"rpm": mid, "torque_percent": tq, "viscosity_cp": pkt.get("viscosity_cp")})

if err < best_err:
best_err, best_rpm = err, mid
if err <= TOL_PCT:
break
if tq < TARGET_TORQUE_PCT:
lo = mid
else:
hi = mid

# Final hold at best_rpm (if none, fall back to mid of last range)
final_rpm = best_rpm if best_rpm is not None else (lo + hi) / 2.0
client.set_speed(final_rpm)
time.sleep(FINAL_HOLD_S)
final_pkt = client.read_single(timeout=1.0)
client.stop()

# write CSV
with out.open("w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["# Target torque (%)", TARGET_TORQUE_PCT])
w.writerow(["# Tolerance (%)", TOL_PCT])
w.writerow(["# Final RPM", round(final_rpm, 3)])
w.writerow([])
w.writerow(["RPM", "Torque (%)", "Viscosity (cP)"])
for h in history:
w.writerow([round(h["rpm"], 3), h["torque_percent"], h["viscosity_cp"]])
w.writerow([])
w.writerow(["FINAL_RPM", round(final_rpm, 3)])
if final_pkt:
w.writerow(["FINAL_TORQUE_%", final_pkt.get("torque_percent")])
w.writerow(["FINAL_VISCOSITY_cP", final_pkt.get("viscosity_cp")])
return str(out)
42 changes: 42 additions & 0 deletions visc_automated_workflow_V3/src/analysis/dynamic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import csv, time, pathlib
from hardware.viscometer.client import ViscometerClient

def run_dynamic_analysis(results_dir: pathlib.Path, client: ViscometerClient):
RPMS = [2.5, 3.0, 3.5, 4.0, 4.5, 4.5, 4.5, 5.0, 5.5, 6.0]
DWELL_SECONDS = 90.0
SETTLE_SECONDS = 1.0
INTER_PAUSE_SEC = 1.0
CSV_NAME = "dynamic_analysis.csv"

out = results_dir / CSV_NAME
out.parent.mkdir(parents=True, exist_ok=True)

rows = []
for rpm in RPMS:
client.set_speed(float(rpm))
time.sleep(SETTLE_SECONDS)
# dwell at rpm
time.sleep(max(DWELL_SECONDS - SETTLE_SECONDS, 0.0))
pkt = client.read_single(timeout=1.0)
rows.append({
"rpm": float(rpm),
"torque_percent": None if not pkt else pkt.get("torque_percent"),
"torque_valid": None if not pkt else pkt.get("torque_valid"),
"temperature_c": None if not pkt else pkt.get("temperature_c"),
"temp_valid": None if not pkt else pkt.get("temp_valid"),
"viscosity_cp": None if not pkt else pkt.get("viscosity_cp"),
"status": None if not pkt else pkt.get("status"),
"record": None if not pkt else pkt.get("record_number"),
})
client.stop()
time.sleep(INTER_PAUSE_SEC)

with out.open("w", newline="", encoding="utf-8") as f: # Write CSV
w = csv.DictWriter(f, fieldnames=[
"rpm","torque_percent","torque_valid",
"temperature_c","temp_valid","viscosity_cp","status","record"
])
w.writeheader()
for r in rows:
w.writerow(r)
return str(out)
50 changes: 50 additions & 0 deletions visc_automated_workflow_V3/src/analysis/single.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import csv, time, pathlib
from hardware.viscometer.client import ViscometerClient

def run_single_rpm(results_dir: pathlib.Path, client: ViscometerClient):
RPM = 32
TOTAL_SECONDS = 180
SAMPLE_EVERY_SEC = 1
SETTLE_SECONDS = 1.0
CSV_NAME = f"single_rpm_{RPM:.2f}.csv"

out = results_dir / CSV_NAME
out.parent.mkdir(parents=True, exist_ok=True)
client.set_speed(RPM)
time.sleep(SETTLE_SECONDS)
t0 = time.time()
next_t = t0 + SAMPLE_EVERY_SEC
rows = []

while True:
now = time.time()
if now - t0 >= TOTAL_SECONDS:
break
if now >= next_t:
pkt = client.read_single(timeout=1.0)
if pkt:
rows.append({
"t_elapsed_s": round(now - t0, 2),
"rpm": RPM,
"torque_percent": pkt.get("torque_percent"),
"torque_valid": pkt.get("torque_valid"),
"temperature_c": pkt.get("temperature_c"),
"temp_valid": pkt.get("temp_valid"),
"viscosity_cp": pkt.get("viscosity_cp"),
"status": pkt.get("status"),
"record": pkt.get("record_number"),
})
next_t += SAMPLE_EVERY_SEC
else:
time.sleep(0.05)

client.stop()
with out.open("w", newline="", encoding="utf-8") as f: # Write CSV
w = csv.DictWriter(f, fieldnames=[
"t_elapsed_s","rpm","torque_percent","torque_valid",
"temperature_c","temp_valid","viscosity_cp","status","record"
])
w.writeheader()
for r in rows:
w.writerow(r)
return str(out)
Empty file.
Empty file.
Loading