Compare commits
5 Commits
SettingTab
...
59528eb6c9
| Author | SHA1 | Date | |
|---|---|---|---|
| 59528eb6c9 | |||
| 9cd3170e8a | |||
| 9779baa468 | |||
| af170666e8 | |||
| 16d8bf48af |
+8
-2
@@ -5,6 +5,9 @@ __pycache__/
|
||||
*.pyd
|
||||
*.so
|
||||
*.a
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
|
||||
# Virtual environment
|
||||
.venv/
|
||||
@@ -14,10 +17,11 @@ env/
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
.qtcreator/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
docs
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -34,4 +38,6 @@ build/
|
||||
dist/
|
||||
*.spec
|
||||
*.icns
|
||||
*.ico
|
||||
*.ico
|
||||
# Superpowers brainstorming visual companion
|
||||
.superpowers/
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import type QmlProjectModel 1.0
|
||||
|
||||
QmlProject {
|
||||
name: "ISC"
|
||||
version: "1.0"
|
||||
mainFile: "src/pygui/ISC/Main.qml"
|
||||
|
||||
dependencies {
|
||||
module: "QtQuick"
|
||||
minimumVersion: "6.0.0"
|
||||
}
|
||||
|
||||
importPaths: [
|
||||
"src/pygui"
|
||||
]
|
||||
}
|
||||
@@ -16,34 +16,42 @@ python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install -e . # install the `pygui` package (src/ layout) in editable mode
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
source .venv/bin/activate
|
||||
python main.py
|
||||
python -m pygui # or run the `isc` console script
|
||||
```
|
||||
|
||||
This launches the Qt window and loads the `ISC` QML module from `ISC/Main.qml`.
|
||||
This launches the Qt window and loads the `ISC` QML module from `src/pygui/ISC/Main.qml`.
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `main.py`: PySide6 bootstrap; creates the Qt app and loads QML module `ISC/Main`.
|
||||
- `ISC/Main.qml`: top-level window definition.
|
||||
- `ISC/HomePage.qml`: main UI layout (left action rail, workspace panel, footer tabs).
|
||||
- `ISC/Theme.qml`: shared theme constants and dark/light mode tokens.
|
||||
- `ISC/qmldir`: QML module registration.
|
||||
The application lives under a `src/` layout as the `pygui` package:
|
||||
|
||||
- `src/pygui/__main__.py`: PySide6 bootstrap; creates the Qt app and loads QML module `ISC/Main`. Entry point for `python -m pygui`.
|
||||
- `src/pygui/backend/`: Qt-facing models and controllers (device, settings, file browser, wafer parsing).
|
||||
- `src/pygui/serialcomm/`: serial port, device service, and data-parser layer.
|
||||
- `src/pygui/ISC/`: the `ISC` QML module (UI).
|
||||
- `Main.qml`: top-level window definition.
|
||||
- `HomePage.qml`: main UI layout (left action rail, workspace panel, footer tabs).
|
||||
- `Theme.qml`: shared theme constants and dark/light mode tokens.
|
||||
- `qmldir`: QML module registration.
|
||||
- `tests/`: pytest suite.
|
||||
- `packaging/`: PyInstaller spec (`isc.spec`) and app icons.
|
||||
|
||||
## Window Configuration
|
||||
|
||||
Window dimensions and constraints are defined in `ISC/Main.qml`:
|
||||
Window dimensions and constraints are defined in `src/pygui/ISC/Main.qml`:
|
||||
|
||||
- **Default size**: 1400 × 820 pixels
|
||||
- **Minimum size**: 1100 × 700 pixels
|
||||
- **Title bar**: "ISenseCloud"
|
||||
|
||||
To adjust the window, edit the `Window` block in `ISC/Main.qml`:
|
||||
To adjust the window, edit the `Window` block in `src/pygui/ISC/Main.qml`:
|
||||
|
||||
```qml
|
||||
Window {
|
||||
@@ -58,8 +66,8 @@ Window {
|
||||
|
||||
## Customization
|
||||
|
||||
- Toggle dark/light mode in `ISC/Theme.qml` via `isDarkMode`.
|
||||
- Update sidebar and footer labels in `ISC/HomePage.qml` through `sideActions` and `bottomTabs`.
|
||||
- Toggle dark/light mode in `src/pygui/ISC/Theme.qml` via `isDarkMode`.
|
||||
- Update sidebar and footer labels in `src/pygui/ISC/HomePage.qml` through `sideActions` and `bottomTabs`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# This will be the name of the output files
|
||||
name: aepwafer
|
||||
wafers: ["A", "E", "P"]
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/edge size of the wafer
|
||||
size: 300
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: large
|
||||
font_size: large
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top implies that the X/Y values
|
||||
# for that axis are negative, or else we'll be drawing outside the image.
|
||||
x_origin: left
|
||||
y_origin: bottom
|
||||
|
||||
# sensor X and Y values, in mm relative to the origin
|
||||
X: [150, 204, 249, 280, 290, 280, 249, 204, 150, 97, 51, 21, 10, 21, 51, 97,
|
||||
150, 203, 241, 255, 241, 203, 150, 98, 59, 45, 59, 98, 171, 207, 228, 228,
|
||||
207, 171, 130, 94, 73, 73, 94, 130, 150, 186, 200, 186, 150, 115, 100, 115]
|
||||
Y: [290, 280, 249, 204, 150, 97, 51, 21, 10, 21, 51, 97, 150, 204, 249, 280,
|
||||
255, 241, 203, 150, 98, 59, 45, 59, 98, 150, 203, 241, 228, 207, 171, 130,
|
||||
94, 73, 73, 94, 130, 171, 207, 228, 200, 186, 150, 115, 100, 115, 150, 186]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 1
|
||||
|
||||
# Normally, positive X is to the right, and positive Y is up.
|
||||
# These settings can be used to reverse one or both of these.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
4: { top: [0, 0] },
|
||||
5: { top: [0, 0] },
|
||||
6: { top: [0, 0] },
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
# This will be the name of the output files
|
||||
name: bcdwafer
|
||||
wafers: ["B", "C", "D"]
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/edge size of the wafer
|
||||
size: 300
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: large
|
||||
font_size: large
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top implies that the X/Y values
|
||||
# for that axis are negative, or else we'll be drawing outside the image.
|
||||
x_origin: center
|
||||
y_origin: center
|
||||
|
||||
# sensor X and Y values, in mm relative to the origin
|
||||
X: [0, 72.50, 125.57, 145.00, 125.57, 72.50, 0, -72.50, -125.57, -145.00,
|
||||
-125.57, -72.50, 0, 47.50, 82.27, 95.00, 82.27, 47.50, 0, -47.50, -82.27,
|
||||
-95.00, -82.27, -47.50, 38.97, 22.50, -38.97, -22.50, 0]
|
||||
Y: [145.00, 125.57, 72.50, 0, -72.50, -125.57, -145.00, -125.57, -72.50, 0,
|
||||
72.50, 125.57, 95.00, 82.27, 47.50, 0, -47.50, -82.27, -95.00, -82.27,
|
||||
-47.50, 0, 47.50, 82.27, 22.50, -38.97, -22.50, 38.97, 0]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 1
|
||||
|
||||
# Normally, positive X is to the right, and positive Y is up.
|
||||
# These settings can be used to reverse one or both of these.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
2: { left: [1, 0] },
|
||||
3: { left: [1, 0] },
|
||||
4: { left: [1, 0] },
|
||||
5: { left: [1, 0] },
|
||||
6: { left: [1, 0] },
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
# This will be the name of the output files
|
||||
name: fwafer
|
||||
wafers: ["F"]
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/edge size of the wafer
|
||||
size: 200
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: large
|
||||
font_size: large
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top implies that the X/Y values
|
||||
# for that axis are negative, or else we'll be drawing outside the image.
|
||||
x_origin: center
|
||||
y_origin: center
|
||||
|
||||
# sensor X and Y values, in mm relative to the origin
|
||||
X: [0.0, 47.5, 82.3, 95.0, 82.3, 47.5, 0.0, -47.5, -82.3, -95.0, -82.3, -47.5,
|
||||
-55.0, -14.8, 39.7, 55.0, 14.8, -39.7, -14.7, 25.5, 14.7, -25.5]
|
||||
Y: [95.0, 82.3, 47.5, 0.0, -47.5, -82.3, -95.0, -82.3, -47.5, 0.0, 47.5, 82.3,
|
||||
13.3, 54.4, 40.0, -13.3, -54.4, -40.0, 25.5, 14.7, -25.5, -14.7]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 1
|
||||
|
||||
# Normally, positive X is to the right, and positive Y is up.
|
||||
# These settings can be used to reverse one or both of these.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
2: { left: [1, 0] },
|
||||
3: { left: [1, 0] },
|
||||
4: { left: [1, 0] },
|
||||
5: { left: [1, 0] },
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
# This will be the name of the output files
|
||||
name: xwafer
|
||||
wafers: ["X"]
|
||||
|
||||
# round or square wafer
|
||||
shape: square
|
||||
|
||||
# diameter/edge size of the wafer
|
||||
size: 310
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: small
|
||||
font_size: small
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top implies that the X/Y values
|
||||
# for that axis are negative, or else we'll be drawing outside the image.
|
||||
x_origin: left
|
||||
y_origin: bottom
|
||||
|
||||
# sensor X and Y values, in mm relative to the origin
|
||||
X: [ 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00,
|
||||
13.00, 23.00, 46.43, 89.86, 133.29, 176.72, 220.15, 263.58, 287.01, 297.01,
|
||||
307.01, 307.01, 307.01, 307.01, 307.01, 307.01, 307.01, 307.01, 307.01,
|
||||
307.01, 307.01, 307.01, 297.01, 287.01, 263.58, 220.15, 176.72, 133.29,
|
||||
89.86, 46.43, 23.00, 13.00, 46.43, 46.43, 46.43, 46.43, 46.43, 46.43,
|
||||
89.86, 133.29, 176.72, 220.15, 263.58, 263.58, 263.58, 263.58, 263.58,
|
||||
263.58, 220.15, 176.72, 133.29, 89.86, 89.86, 89.86, 89.86, 89.86,
|
||||
133.29, 176.72, 220.15, 220.15, 220.15, 220.15, 176.72, 133.29, 133.29,
|
||||
133.29, 176.72, 176.72]
|
||||
Y: [ 3.00, 13.07, 23.07, 46.50, 89.93, 133.36, 176.79, 220.22, 263.65, 287.08,
|
||||
297.08, 307.08, 307.08, 307.08, 307.08, 307.08, 307.08, 307.08, 307.08,
|
||||
307.08, 307.08, 307.08, 307.08, 297.08, 287.08, 263.65, 220.22, 176.79,
|
||||
133.36, 89.93, 46.50, 23.07, 13.07, 3.00, 3.00, 3.00, 3.00, 3.00, 3.00,
|
||||
3.00, 3.00, 3.00, 3.00, 3.00, 46.50, 89.93, 133.36, 176.79, 220.22, 263.65,
|
||||
263.65, 263.65, 263.65, 263.65, 263.65, 220.22, 176.79, 133.36, 89.93,
|
||||
46.50, 46.50, 46.50, 46.50, 46.50, 89.93, 133.36, 176.79, 220.22, 220.22,
|
||||
220.22, 220.22, 176.79, 133.36, 89.93, 89.93, 89.93, 133.36, 176.79,
|
||||
176.79, 133.36 ]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 1
|
||||
|
||||
# Normally, positive X is to the right, and positive Y is up.
|
||||
# These settings can be used to reverse one or both of these.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
21: { left: [0, 0] },
|
||||
22: { left: [0, 0] },
|
||||
23: { left: [0, 0] },
|
||||
24: { left: [0, 0] },
|
||||
25: { left: [0, 0] },
|
||||
26: { left: [0, 0] },
|
||||
27: { left: [0, 0] },
|
||||
28: { left: [0, 0] },
|
||||
29: { left: [0, 0] },
|
||||
30: { left: [0, 0] },
|
||||
31: { left: [0, 0] },
|
||||
32: { left: [0, 0] },
|
||||
33: { left: [0, 0] },
|
||||
34: { left: [0, 0] },
|
||||
35: { left: [0, 0] },
|
||||
36: { left: [0, 0] }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
# This will be the name of the output files
|
||||
name: zwafer
|
||||
wafers: ["Z"]
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/size of the wafer
|
||||
size: 300
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: small
|
||||
font_size: small
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top probably also needs
|
||||
# reverse_x/reverse_y to be set as well.
|
||||
x_origin: center
|
||||
y_origin: center
|
||||
|
||||
# sensor X and Y values, in mm relative to the wafer center
|
||||
X: [0.00, -1.17, 8.92, 1.17, -8.92, 22.52, 29.35, -22.52, -29.35, 45.05, 73.37,
|
||||
58.71, 9.66, -45.05, -73.37, -58.71, -9.66, 79.85, 89.06, 46.10, -23.86,
|
||||
-79.85, -89.06, -46.10, 23.86, 66.96, 109.06, 87.27, 14.36, -66.96,
|
||||
-109.06, -87.27, -14.36, 121.24, 135.23, 70.00, -36.23, -121.24, -135.23,
|
||||
-70.00, 36.23, 124.71, 139.09, 72.00, -37.27, -124.71, -139.09, -72.00,
|
||||
37.27, 89.49, 127.31, 145.74, 141.99, 116.62, 73.50, 19.19, -38.05, -89.49,
|
||||
-127.31, -145.74, -141.99, -116.62, -73.50, -19.19, 38.05]
|
||||
Y: [0.00, -8.92, -1.17, 8.92, 1.17, -29.35, 22.52, 29.35, -22.52, -58.71,
|
||||
-9.66, 45.05, 73.37, 58.71, 9.66, -45.05, -73.37, -46.10, 23.86, 79.85,
|
||||
89.06, 46.10, -23.86, -79.85, -89.06, -87.27, -14.36, 66.96, 109.06, 87.27,
|
||||
14.36, -66.96, -109.06, -70.00, 36.23, 121.24, 135.23, 70.00, -36.23,
|
||||
-121.24, -135.23, -72.00, 37.27, 124.71, 139.09, 72.00, -37.27, -124.71,
|
||||
-139.09, -116.62, -73.50, -19.19, 38.05, 89.49, 127.31, 145.74, 141.99,
|
||||
116.62, 73.50, 19.19, -38.05, -89.49, -127.31, -145.74, -141.99]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 0
|
||||
|
||||
# If one or both axes are reversed, this can be used to fix it
|
||||
# without having to edit all of the coordinates.
|
||||
# These should be false for most normal wafers.
|
||||
reverse_x: false
|
||||
reverse_y: false
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
33: { left: [0, 0] },
|
||||
34: { left: [0, 0] },
|
||||
41: { top: [0, 0] },
|
||||
42: { bottom: [0, 0] },
|
||||
45: { top: [1, 0] },
|
||||
46: { bottom: [0.5, 0] },
|
||||
49: { left: [0, 0] },
|
||||
50: { bottom: [-1.75, -0.5] },
|
||||
51: { top: [-1, 0] },
|
||||
52: { top: [-1, 0] },
|
||||
53: { bottom: [0, 0] },
|
||||
54: { left: [0, -0.75] },
|
||||
55: { right: [0, 0.5] },
|
||||
58: { bottom: [-0.75, 0] },
|
||||
60: { top: [0, 0] },
|
||||
62: { right: [0, 1] },
|
||||
63: { right: [0, 0] },
|
||||
64: { left: [0, 0.25] }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
# This will be the name of the output files
|
||||
name: zwafer_rev
|
||||
wafers: [] # the reversed version doesn't represent any real wafer
|
||||
|
||||
# round or square wafer
|
||||
shape: round
|
||||
|
||||
# diameter/size of the wafer
|
||||
size: 300
|
||||
|
||||
# large or small markers and labels
|
||||
marker_size: small
|
||||
font_size: small
|
||||
|
||||
# origin settings. x_origin can be left, right, or center. y_origin can
|
||||
# be top, bottom, or center. Using right or top probably also needs
|
||||
# reverse_x/reverse_y to be set as well.
|
||||
x_origin: center
|
||||
y_origin: center
|
||||
|
||||
# sensor X and Y values, in mm relative to the wafer center
|
||||
X: [0.00, -1.17, 8.92, 1.17, -8.92, 22.52, 29.35, -22.52, -29.35, 45.05, 73.37,
|
||||
58.71, 9.66, -45.05, -73.37, -58.71, -9.66, 79.85, 89.06, 46.10, -23.86,
|
||||
-79.85, -89.06, -46.10, 23.86, 66.96, 109.06, 87.27, 14.36, -66.96,
|
||||
-109.06, -87.27, -14.36, 121.24, 135.23, 70.00, -36.23, -121.24, -135.23,
|
||||
-70.00, 36.23, 124.71, 139.09, 72.00, -37.27, -124.71, -139.09, -72.00,
|
||||
37.27, 89.49, 127.31, 145.74, 141.99, 116.62, 73.50, 19.19, -38.05, -89.49,
|
||||
-127.31, -145.74, -141.99, -116.62, -73.50, -19.19, 38.05]
|
||||
Y: [0.00, -8.92, -1.17, 8.92, 1.17, -29.35, 22.52, 29.35, -22.52, -58.71,
|
||||
-9.66, 45.05, 73.37, 58.71, 9.66, -45.05, -73.37, -46.10, 23.86, 79.85,
|
||||
89.06, 46.10, -23.86, -79.85, -89.06, -87.27, -14.36, 66.96, 109.06, 87.27,
|
||||
14.36, -66.96, -109.06, -70.00, 36.23, 121.24, 135.23, 70.00, -36.23,
|
||||
-121.24, -135.23, -72.00, 37.27, 124.71, 139.09, 72.00, -37.27, -124.71,
|
||||
-139.09, -116.62, -73.50, -19.19, 38.05, 89.49, 127.31, 145.74, 141.99,
|
||||
116.62, 73.50, 19.19, -38.05, -89.49, -127.31, -145.74, -141.99]
|
||||
|
||||
# set this to either 0 or 1 for the initial sensor number (usually 1)
|
||||
start_sn: 0
|
||||
|
||||
# If one or both axes are reversed, this can be used to fix it
|
||||
# without having to edit all of the coordinates.
|
||||
# These should be false for most normal wafers.
|
||||
reverse_x: true
|
||||
reverse_y: true
|
||||
|
||||
# these are sensors whose labels need to be repositioned. Sensor numbers
|
||||
# start at start_sn. Directions are up, down, left, and right, and values
|
||||
# are multiples of the marker size.
|
||||
label_exceptions: {
|
||||
37: { left: [0, 0] },
|
||||
38: { left: [0, 0] },
|
||||
39: { left: [0, 0] },
|
||||
41: { top: [1.5, 0] },
|
||||
42: { bottom: [1, 0] },
|
||||
43: { left: [0, -1] },
|
||||
45: { bottom: [-1, -1] },
|
||||
46: { bottom: [0, -1] },
|
||||
47: { right: [0, 1] },
|
||||
48: { left: [0, 0] },
|
||||
50: { bottom: [-1, 0] },
|
||||
52: { top: [0, 0] },
|
||||
54: { right: [0.5, 1] },
|
||||
56: { left: [0, 0.5] },
|
||||
57: { left: [0, 0] },
|
||||
58: { top: [0.5, 0] },
|
||||
59: { left: [0, 0] },
|
||||
60: { top: [-1, 0] },
|
||||
61: { left: [0, 0] },
|
||||
62: { left: [0, -1] },
|
||||
63: { right: [0, 1] },
|
||||
}
|
||||
+47
-1
@@ -1,3 +1,8 @@
|
||||
# ===== Build System =====
|
||||
[build-system]
|
||||
requires = ["setuptools>=61"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
# ===== Project Metadata =====
|
||||
[project]
|
||||
name = "pygui"
|
||||
@@ -6,9 +11,50 @@ version = "0.1.0"
|
||||
[project.optional-dependencies]
|
||||
dev = ["pytest"]
|
||||
|
||||
# ===== Console Entry Point =====
|
||||
[project.scripts]
|
||||
isc = "pygui.__main__:main"
|
||||
|
||||
# ===== Package Discovery (src layout) =====
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
# Ship the QML module alongside the Python package.
|
||||
pygui = ["ISC/**/*.qml", "ISC/**/qmldir"]
|
||||
|
||||
# ===== PySide Build Inputs =====
|
||||
[tool.pyside6-project]
|
||||
files = ["ISC/HomePage.qml", "ISC/Main.qml", "ISC/Tabs/DataTab.qml", "ISC/Tabs/GraphTab.qml", "ISC/Tabs/SelectFileDialog.qml", "ISC/Tabs/SettingsTab.qml", "ISC/Tabs/StatusTab.qml", "ISC/Tabs/qmldir", "ISC/Theme.qml", "ISC/qmldir", "backend/data_model.py", "backend/device_controller.py", "backend/graph_view.py", "backend/file_browser.py", "backend/local_settings.py", "backend/local_settings_model.py", "main.py", "serialcomm/__init__.py", "serialcomm/data_parser.py", "serialcomm/serial_port.py", "serialcomm/device_service.py"]
|
||||
files = [
|
||||
"src/pygui/ISC/HomePage.qml",
|
||||
"src/pygui/ISC/Main.qml",
|
||||
"src/pygui/ISC/Theme.qml",
|
||||
"src/pygui/ISC/qmldir",
|
||||
"src/pygui/ISC/Tabs/DataTab.qml",
|
||||
"src/pygui/ISC/Tabs/SelectFileDialog.qml",
|
||||
"src/pygui/ISC/Tabs/SettingsTab.qml",
|
||||
"src/pygui/ISC/Tabs/StatusTab.qml",
|
||||
"src/pygui/ISC/Tabs/qmldir",
|
||||
"src/pygui/__main__.py",
|
||||
"src/pygui/backend/contour_models.py",
|
||||
"src/pygui/backend/crypto_helper.py",
|
||||
"src/pygui/backend/csv_file_metadata.py",
|
||||
"src/pygui/backend/data_model.py",
|
||||
"src/pygui/backend/data_segment.py",
|
||||
"src/pygui/backend/device_controller.py",
|
||||
"src/pygui/backend/file_browser.py",
|
||||
"src/pygui/backend/frame.py",
|
||||
"src/pygui/backend/graph_view.py",
|
||||
"src/pygui/backend/local_settings.py",
|
||||
"src/pygui/backend/local_settings_model.py",
|
||||
"src/pygui/backend/marching_squares.py",
|
||||
"src/pygui/backend/zwafer_models.py",
|
||||
"src/pygui/backend/zwafer_parser.py",
|
||||
"src/pygui/serialcomm/__init__.py",
|
||||
"src/pygui/serialcomm/data_parser.py",
|
||||
"src/pygui/serialcomm/device_service.py",
|
||||
"src/pygui/serialcomm/serial_port.py",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
|
||||
@@ -92,6 +92,13 @@ QtObject {
|
||||
readonly property color statusWarningColor: isDarkMode ? "#F5C15C" : "#C88A18"
|
||||
readonly property color statusErrorColor: isDarkMode ? "#FF6B6B" : "#D64545"
|
||||
|
||||
// -- 10b. Sensor bands (wafer map dots)
|
||||
readonly property color sensorInRange: statusSuccessColor
|
||||
readonly property color sensorHigh: statusErrorColor
|
||||
readonly property color sensorlow: isDarkMode ? "#589DF5" : "#2F6FE0"
|
||||
readonly property color waferRingColor: toneBorder
|
||||
readonly property color waferAxisColor: softBorder
|
||||
|
||||
// ── 11. Geometry ─────────────────────────────────────────────────────────
|
||||
// Radius
|
||||
readonly property int radiusXs: 4 // fields, tight elements
|
||||
@@ -5,13 +5,14 @@ from PySide6.QtQml import QQmlApplicationEngine
|
||||
from PySide6.QtQuickControls2 import QQuickStyle
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from backend.device_controller import DeviceController
|
||||
from backend.local_settings import LocalSettings
|
||||
from backend.local_settings_model import LocalSettingsModel
|
||||
from backend.file_browser import FileBrowser
|
||||
from pygui.backend.device_controller import DeviceController
|
||||
from pygui.backend.local_settings import LocalSettings
|
||||
from pygui.backend.local_settings_model import LocalSettingsModel
|
||||
from pygui.backend.file_browser import FileBrowser
|
||||
|
||||
|
||||
# ===== Application Entry Point =====
|
||||
if __name__ == "__main__":
|
||||
def main() -> int:
|
||||
# ===== UI Style Setup =====
|
||||
# Use a non-native controls style so our custom QML button backgrounds are supported.
|
||||
QQuickStyle.setStyle("Basic")
|
||||
@@ -35,11 +36,17 @@ if __name__ == "__main__":
|
||||
engine.rootContext().setContextProperty("deviceController", device_controller)
|
||||
|
||||
# ===== QML Startup =====
|
||||
# The "ISC" QML module lives alongside this file (src/pygui/ISC), so the
|
||||
# package directory is the import path the engine searches for qmldir.
|
||||
engine.addImportPath(Path(__file__).parent)
|
||||
engine.loadFromModule("ISC", "Main")
|
||||
|
||||
# ===== Exit Handling =====
|
||||
if not engine.rootObjects():
|
||||
sys.exit(-1)
|
||||
return -1
|
||||
|
||||
sys.exit(app.exec())
|
||||
return app.exec()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -15,17 +15,17 @@ from typing import Any, Optional
|
||||
|
||||
from PySide6.QtCore import QObject, Property, Qt, Signal, Slot
|
||||
|
||||
from backend.data_model import TemperatureTableModel
|
||||
from backend.graph_view import GraphView
|
||||
from backend.local_settings import LocalSettings
|
||||
from serialcomm.data_parser import (
|
||||
from pygui.backend.data_model import TemperatureTableModel
|
||||
from pygui.backend.graph_view import GraphView
|
||||
from pygui.backend.local_settings import LocalSettings
|
||||
from pygui.serialcomm.data_parser import (
|
||||
convert_to_temperatures,
|
||||
parse_binary_data,
|
||||
remove_trailing_zeros,
|
||||
save_to_csv,
|
||||
)
|
||||
from serialcomm.device_service import DeviceService
|
||||
from serialcomm.serial_port import WaferInfo
|
||||
from pygui.serialcomm.device_service import DeviceService
|
||||
from pygui.serialcomm.serial_port import WaferInfo
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -8,8 +8,8 @@ from pathlib import Path
|
||||
from PySide6.QtCore import QObject, Property, QStandardPaths, Signal, Slot
|
||||
from PySide6.QtWidgets import QFileDialog, QMessageBox
|
||||
|
||||
from backend.csv_file_metadata import CSVFileMetadata
|
||||
from backend.zwafer_parser import ZWaferParser
|
||||
from pygui.backend.csv_file_metadata import CSVFileMetadata
|
||||
from pygui.backend.zwafer_parser import ZWaferParser
|
||||
|
||||
|
||||
# ===== File Browser Model =====
|
||||
@@ -0,0 +1,11 @@
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Frame:
|
||||
"""One sample across all sensors ata a point in time"""
|
||||
|
||||
seq: int # monotonically increasing
|
||||
t: float # seconds (relative or epoch)
|
||||
values: list[float] # one per sensor, in sensor-layout order
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Per-frame descriptive statistics"""
|
||||
|
||||
from __future__ import annotations
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Stats:
|
||||
min: float; min_index: int
|
||||
max: float; max_index: int
|
||||
diff: float; avg: float
|
||||
sigma: float; three_sigma: float
|
||||
|
||||
|
||||
def compute_stats(values: list[float]) -> Stats:
|
||||
clean = [(i, v) for i, v in enumerate(values) if not math.isnan(v)]
|
||||
if not clean:
|
||||
return Stats(0.0, -1, 0.0, -1, 0.0, 0.0, 0.0, 0.0)
|
||||
|
||||
min_index, min_v = min(clean, key=lambda iv: iv[1])
|
||||
max_index, max_v = max(clean, key=lambda iv: iv[1])
|
||||
nums = [v for _, v in clean]
|
||||
avg = sum(nums) / len(nums)
|
||||
variance = sum((v - avg) ** 2 for v in nums) / len(nums)
|
||||
sigma = math.sqrt(variance)
|
||||
|
||||
return Stats(
|
||||
min=min_v,
|
||||
min_index=min_index,
|
||||
max=max_v,
|
||||
max_index=max_index,
|
||||
diff=max_v - min_v,
|
||||
avg=avg,
|
||||
sigma=sigma,
|
||||
three_sigma=3 * sigma,
|
||||
)
|
||||
@@ -6,7 +6,7 @@ from typing import Any
|
||||
|
||||
from PySide6.QtCore import QObject, Property, QDateTime, QStandardPaths, Signal, Slot
|
||||
|
||||
from backend.local_settings import LocalSettings
|
||||
from pygui.backend.local_settings import LocalSettings
|
||||
|
||||
|
||||
MASTER_FAMILIES = ("A", "B", "C", "D", "E", "F", "P", "X", "Z")
|
||||
@@ -2,7 +2,7 @@ from typing import List, Tuple, Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
from backend.contour_models import ContourLine, ContourSegment
|
||||
from pygui.backend.contour_models import ContourLine, ContourSegment
|
||||
|
||||
|
||||
# ===== Contour Generation =====
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Detect process state (Idle, Ramp, Set) from the running average temp"""
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
STATE_IDLE = "idle"
|
||||
STATE_RAMP = "ramp"
|
||||
STATE_SET = "set"
|
||||
|
||||
|
||||
class StabilityDetector:
|
||||
def __init__(self, idle_below: float = 50.0, tolerance: float = 1.0,
|
||||
settle_seconds: float = 10.0) -> None:
|
||||
self._idle_below = idle_below
|
||||
self._tolerance = tolerance
|
||||
self._settle_seconds = settle_seconds
|
||||
self._near_since: Optional[float] = None # When avg entered the +- tolerance band
|
||||
|
||||
|
||||
def reset(self) -> None:
|
||||
self._near_since = None
|
||||
|
||||
def update(self, avg: float, time: float, set_point: float) ->str:
|
||||
if avg < self._idle_below:
|
||||
self._near_since = None
|
||||
return STATE_IDLE
|
||||
if abs(avg - set_point) <= self._tolerance:
|
||||
if self._near_since is None:
|
||||
self._near_since = time
|
||||
if time - self._near_since >= self._settle_seconds:
|
||||
return STATE_SET
|
||||
return STATE_RAMP
|
||||
self._near_since = None
|
||||
return STATE_RAMP
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Classify sensor values into three bands around (target, margin)
|
||||
|
||||
Auto mode derives target=mean, margin=1
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
BAND_IN = "in_range"
|
||||
BAND_HIGH = "high"
|
||||
BAND_LOW = "low"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ThresholdConfig:
|
||||
set_point: float = 149.0 # process target: used as band TARGET when auto=False
|
||||
margin: float = 1.0 # used as band MARGIN when auto=False
|
||||
auto: bool = True # auto=True: target=frame mean, margin=frame 1σ
|
||||
|
||||
def resolve_bounds(values: list[float], cfg: ThresholdConfig) -> tuple[float, float]:
|
||||
if not cfg.auto:
|
||||
return cfg.set_point, cfg.margin
|
||||
clean = [v for v in values if not math.isnan(v)]
|
||||
if not clean:
|
||||
return cfg.set_point, cfg.margin
|
||||
mean = sum(clean) / len(clean)
|
||||
variance = sum((v - mean) ** 2 for v in clean) / len(clean)
|
||||
return mean, math.sqrt(variance)
|
||||
|
||||
def classify(value: float, target: float, margin: float) -> str:
|
||||
if math.isnan(value):
|
||||
return BAND_IN
|
||||
if value > target + margin:
|
||||
return BAND_HIGH
|
||||
if value < target - margin:
|
||||
return BAND_LOW
|
||||
return BAND_IN
|
||||
|
||||
def classify_all(values: list[float], cfg: ThresholdConfig) -> list[str]:
|
||||
target, margin = resolve_bounds(values, cfg)
|
||||
return [classify(v, target, margin)for v in values ]
|
||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
||||
from typing import Tuple, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from backend.zwafer_models import ZWaferData, Sensor
|
||||
from pygui.backend.zwafer_models import ZWaferData, Sensor
|
||||
|
||||
|
||||
# ===== Z-Wafer CSV Parser =====
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Serial port communication layer for the temperature-sensing wafer."""
|
||||
|
||||
from serialcomm.device_service import DeviceService
|
||||
from serialcomm.serial_port import SerialPort, WaferInfo
|
||||
from pygui.serialcomm.device_service import DeviceService
|
||||
from pygui.serialcomm.serial_port import SerialPort, WaferInfo
|
||||
|
||||
__all__ = ["DeviceService", "SerialPort", "WaferInfo"]
|
||||
@@ -11,8 +11,8 @@ from typing import Optional
|
||||
|
||||
import serial.tools.list_ports
|
||||
|
||||
from backend.local_settings import LocalSettings
|
||||
from serialcomm.serial_port import SerialPort, WaferInfo
|
||||
from pygui.backend.local_settings import LocalSettings
|
||||
from pygui.serialcomm.serial_port import SerialPort, WaferInfo
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tests for serialcomm/data_parser.py binary parsing pipeline."""
|
||||
|
||||
import pytest
|
||||
from serialcomm.data_parser import (
|
||||
from pygui.serialcomm.data_parser import (
|
||||
csv_column_count,
|
||||
parse_binary_data,
|
||||
convert_to_temperatures,
|
||||
@@ -12,7 +12,6 @@ from serialcomm.data_parser import (
|
||||
MAXDUT_X,
|
||||
)
|
||||
|
||||
|
||||
# ── csv_column_count ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -203,6 +202,7 @@ class TestConvertToTemperatures:
|
||||
def test_p_family_single_block(self):
|
||||
data = _make_p_block(1, value=0x0100)
|
||||
hex_data = parse_binary_data(data, "P")
|
||||
assert hex_data is not None
|
||||
result = convert_to_temperatures(hex_data, "P")
|
||||
assert len(result) == 1
|
||||
assert all(isinstance(v, str) for v in result[0])
|
||||
@@ -270,9 +270,9 @@ class TestSaveToCsv:
|
||||
result = save_to_csv(data, family, f"{family}00001", str(tmp_path))
|
||||
assert result is not None, f"save_to_csv returned None for {family}"
|
||||
headers = open(result).readline().strip().split(",")
|
||||
assert len(headers) == expected_cols, (
|
||||
f"{family}: expected {expected_cols} headers, got {len(headers)}"
|
||||
)
|
||||
assert (
|
||||
len(headers) == expected_cols
|
||||
), f"{family}: expected {expected_cols} headers, got {len(headers)}"
|
||||
assert headers[0] == "Sensor1"
|
||||
assert headers[-1] == f"Sensor{expected_cols}"
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import math
|
||||
import pytest
|
||||
from pygui.backend.frame_stats import compute_stats, Stats
|
||||
|
||||
|
||||
def test_basic_stat():
|
||||
s = compute_stats([148.0, 150.0, 149.0])
|
||||
assert s.min == 148.0 and s.min_index == 0
|
||||
assert s.max == 150.0 and s.max_index == 1
|
||||
assert s.diff == pytest.approx(2.0)
|
||||
assert s.avg == pytest.approx(149.0)
|
||||
assert s.sigma == pytest.approx(math.sqrt(2 / 3))
|
||||
assert s.three_sigma == pytest.approx(3 * math.sqrt(2 / 3))
|
||||
|
||||
|
||||
def test_empty_values_returns_zeros():
|
||||
s = compute_stats([])
|
||||
assert s == Stats(0.0, -1, 0.0, -1, 0.0, 0.0, 0.0, 0.0)
|
||||
|
||||
|
||||
def test_ignores_nan():
|
||||
s = compute_stats([149.0, float("nan"), 151.0])
|
||||
@@ -0,0 +1,35 @@
|
||||
from pygui.backend.stability_detector import (
|
||||
StabilityDetector, STATE_IDLE, STATE_RAMP, STATE_SET,
|
||||
)
|
||||
|
||||
|
||||
SET_POINT = 149.0
|
||||
|
||||
def make():
|
||||
return StabilityDetector(idle_below = 50.0, tolerance=1.0, settle_seconds=2.0)
|
||||
|
||||
def test_idle_when_cold():
|
||||
d = make()
|
||||
assert d.update(avg=25.0, time=0.0, set_point=SET_POINT) == STATE_IDLE
|
||||
|
||||
def test_ramp_while_far_from_setpoint():
|
||||
d = make()
|
||||
d.update(avg=100.0, time=0.0, set_point=SET_POINT)
|
||||
|
||||
def test_ramp_until_settle_time_elapses():
|
||||
d = make()
|
||||
assert d.update(avg=149.2, time=0.0, set_point=SET_POINT) == STATE_RAMP
|
||||
assert d.update(avg=148.9, time=0.0, set_point=SET_POINT) == STATE_RAMP
|
||||
|
||||
def test_set_after_holding_near_setpoint():
|
||||
d = make()
|
||||
d.update(avg=149.2, time=0.0, set_point=SET_POINT)
|
||||
d.update(avg=148.9, time=1.0, set_point=SET_POINT)
|
||||
assert d.update(avg=149.0, time=2.5, set_point=SET_POINT) == STATE_SET
|
||||
|
||||
|
||||
def test_back_to_ramp_on_disturbance():
|
||||
d = make()
|
||||
for t in (0.0, 1.0, 2.5):
|
||||
d.update(149.0, t, set_point=SET_POINT)
|
||||
assert d.update(avg=160.0, time=3.0, set_point=SET_POINT) == STATE_RAMP
|
||||
@@ -0,0 +1,29 @@
|
||||
import math
|
||||
|
||||
from pygui.backend.threshold_classifier import (
|
||||
ThresholdConfig, classify, classify_all, resolve_bounds,
|
||||
BAND_IN, BAND_HIGH, BAND_LOW
|
||||
)
|
||||
|
||||
|
||||
def test_classify_about_target_margin():
|
||||
assert classify(149.0, target=149.0, margin=1.0) ==BAND_IN # exactly
|
||||
assert classify(149.9, target=149.0, margin=1.0) ==BAND_IN # within
|
||||
assert classify(150.5, target=149.0, margin=1.0) ==BAND_HIGH # 1.5 above
|
||||
assert classify(147.5, target=149.0, margin=1.0) ==BAND_LOW # 1.5 below
|
||||
|
||||
|
||||
def test_manual_bounds_use_set_point_and_margin():
|
||||
cfg = ThresholdConfig(set_point=149.0, margin=1.0, auto=False)
|
||||
assert resolve_bounds([200.0, 0.0], cfg) == (149.0, 1.0)
|
||||
|
||||
def test_auto_bounds_use_mean_and_sigma():
|
||||
cfg = ThresholdConfig(auto=True)
|
||||
target, margin = resolve_bounds([148.0, 150.0, 149.0], cfg)
|
||||
assert target == 149.0
|
||||
assert margin == math.sqrt(2 / 3)
|
||||
|
||||
def test_classify_all_manual():
|
||||
cfg = ThresholdConfig(set_point=149.0, margin=1.0, auto=False)
|
||||
assert classify_all([149.0, 151.0, 147.0], cfg) == [BAND_IN, BAND_HIGH, BAND_LOW]
|
||||
|
||||
Reference in New Issue
Block a user