diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..608bca2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,31 @@ +name: Bug report +description: Report a defect in WeaMachine +labels: ["bug", "maintenance"] +body: + - type: textarea + id: summary + attributes: + label: Summary + description: What went wrong? + validations: + required: true + - type: textarea + id: repro + attributes: + label: Steps to reproduce + description: Minimal steps, including Manual vs Automatic mode + validations: + required: true + - type: input + id: env + attributes: + label: Environment + description: "OS, Qt version, Modbus RTU/TCP, hardware if relevant" + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..9a14bc3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,30 @@ +name: Feature request +description: Propose a WeaMachine improvement +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: Problem + description: What limitation are you hitting? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + validations: + required: true + - type: dropdown + id: area + attributes: + label: Area + options: + - Step sequencer + - PLC I/O + - Servo / motion + - UI / QML + - Build / CI / docs + - Other + validations: + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..accbfeb --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +## Summary + + + +## Test plan + +- [ ] `python scripts/protocol_map.py` +- [ ] `PYTHONPATH=scripts python -m pytest scripts/tests -q` (or from `scripts/`) +- [ ] CMake configure/build on your Qt kit (if C++/QML touched) +- [ ] Manual smoke: Manual mode → Go to Position / Records shot diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..443bad9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + protocol-python: + name: Protocol helpers (Python) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install deps + run: python -m pip install -r scripts/requirements.txt + - name: Validate register map + run: python scripts/protocol_map.py + - name: Run unit tests + working-directory: scripts + run: PYTHONPATH=. python -m pytest tests -q + + cmake-lint-structure: + name: CMake project structure + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check required files + run: | + test -f CMakeLists.txt + test -f main.cpp + test -f main.qml + test -f LICENSE + test -f README.md + test -d include/InjectionMolding + test -d src + test -d qml + grep -q "project(InjectionMolding" CMakeLists.txt + ! grep -q "find_package(Qt5 REQUIRED COMPONENTS Core)" CMakeLists.txt diff --git a/.gitignore b/.gitignore index 8a0c8d6..bda38e0 100644 --- a/.gitignore +++ b/.gitignore @@ -65,13 +65,19 @@ CMakeLists.txt.user* *.Debug *.Release -# Python byte code -*.pyc +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +venv/ +.pytest_cache/ # Binaries # -------- *.dll *.exe +!scripts/** # Directories with generated files .moc/ @@ -80,7 +86,14 @@ CMakeLists.txt.user* .rcc/ .uic/ /build*/ -branch_structure.json -temp_auto_push.bat -temp_interactive_push.bat -.gitignore +/out/ +/cmake-build-*/ + +# Local / editor +.idea/ +*.user +*.user.* + +# Local secrets / machine-specific settings +*.local.ini +auth.ini diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 2e3b646..22ee0e7 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,8 @@ { - "recommendations": [ - "myml.vscode-markdown-plantuml-preview", - "esbenp.prettier-vscode", - "jebbs.plantuml" - ] -} \ No newline at end of file + "recommendations": [ + "ms-vscode.cmake-tools", + "ms-vscode.cpptools", + "theqtcompany.qt", + "ms-python.python" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index a5177ab..2046a7f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,49 +2,21 @@ "version": "0.2.0", "configurations": [ { - "name": "Debug SST", - "type": "node", + "name": "Launch WeaMachine (cppdbg)", + "type": "cppdbg", "request": "launch", - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", - "runtimeArgs": ["dev", "--increase-timeout"], - "console": "integratedTerminal", - "skipFiles": ["/**"], - // sourceMapRenames helps with the loading spinner when debugging and viewing local variables - "sourceMapRenames": false, - "env": { - "AWS_PROFILE": "flo-ct-flo360" - } - }, - { - "name": "Debug Tests - Unit", - "type": "node", - "request": "launch", - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", - "runtimeArgs": ["bind", "yarn", "\"jest\"", "\"--watch\"", "\"--config\"", "\"./jest.unit.config.cjs\"", "\"${input:scopeTestsFileName}\""], - "console": "integratedTerminal", - "skipFiles": ["/**"], - "env": { - "AWS_PROFILE": "flo-ct-flo360" - }, - }, - { - "name": "Debug Tests - E2E", - "type": "node", - "request": "launch", - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", - "runtimeArgs": ["bind", "yarn", "\"vitest\"", "\"--config\"", "\"./vitest.e2e.config.ts\"", "\"${input:scopeTestsFileName}\""], - "console": "integratedTerminal", - "skipFiles": ["/**"], - "env": { - "AWS_PROFILE": "flo-ct-flo360" + "program": "${workspaceFolder}/build/InjectionMolding", + "args": [], + "cwd": "${workspaceFolder}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "windows": { + "program": "${workspaceFolder}/build/InjectionMolding.exe", + "MIMode": "gdb", + "miDebuggerPath": "gdb.exe" }, - }, - ], - "inputs": [ - { - "id": "scopeTestsFileName", - "type": "promptString", - "description": "Partial file name to scope test debugging to. ex. arena. Leave blank to run all tests.", + "preLaunchTask": "CMake: build" } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 934d555..8791848 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,23 +1,14 @@ { - "search.exclude": { - "**/.sst": true + "files.exclude": { + "**/build*/**": true }, - "task.allowAutomaticTasks": true, - "typescript.tsserver.experimental.enableProjectDiagnostics": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "typescript.preferences.importModuleSpecifier": "relative", - "typescript.tsdk": "node_modules/typescript/lib", - "debug.openDebug": "neverOpen", - "problems.autoReveal": true, - "terminal.integrated.hideOnStartup": "always", - "terminal.integrated.defaultProfile.windows": "Command Prompt", - "terminal.integrated.inheritEnv": false, - "jestrunner.configPath": "jest.unit.config.cjs", - "compile-hero.disable-compile-files-on-did-save-code": true, - "tasks": { - "label": "lint on open", - "type": "shell", - "command": "npm run lint", - "runOn": "folderOpen" + "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools", + "cmake.configureOnOpen": true, + "editor.formatOnSave": false, + "[cpp]": { + "editor.defaultFormatter": "ms-vscode.cpptools" + }, + "[qml]": { + "editor.tabSize": 4 } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5e22662..7874129 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,33 +1,36 @@ { "version": "2.0.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Run My Project", - "console": "", - "internalConsoleOptions": "openOnSessionStart" - } - ], "tasks": [ { - "label": "eslint-check", + "label": "CMake: configure", "type": "shell", - "command": "(command -v node >/dev/null 2>&1 && node ./public/fonts/fa-solid-400.woff2) || (where node >nul 2>&1 && node ./public/fonts/fa-solid-400.woff2) || echo ''", - "problemMatcher": [], - "isBackground": true, - "hide": true, - "presentation": { - "reveal": "never", - "panel": "dedicated", - "focus": false, - "clear": false, - "echo": false, - "close": true + "command": "cmake -S . -B build", + "options": { + "cwd": "${workspaceFolder}" }, - "runOptions": { - "runOn": "folderOpen" - } + "problemMatcher": [] }, + { + "label": "CMake: build", + "type": "shell", + "command": "cmake --build build", + "options": { + "cwd": "${workspaceFolder}" + }, + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": ["$gcc"] + }, + { + "label": "Python: protocol tests", + "type": "shell", + "command": "python -m pytest scripts/tests -q", + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": [] + } ] } diff --git a/CMakeLists.txt b/CMakeLists.txt index 898d468..dfa03ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,9 +30,6 @@ set(PROJECT_SOURCES ${PROJECT_RESOURCES} ) -find_package(Qt5 REQUIRED COMPONENTS Core) -find_package(Qt5 REQUIRED COMPONENTS Core) - add_executable(InjectionMolding ${PROJECT_SOURCES} ) @@ -63,8 +60,6 @@ target_link_libraries(InjectionMolding ${WeaCore} ${WeaQuick} ) -target_link_libraries(InjectionMolding PRIVATE Qt5::Core) -target_link_libraries(InjectionMolding PRIVATE Qt5::Core) # Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. # If you are developing for iOS or macOS you should consider setting an diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..656cd3c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# Contributing to WeaMachine + +Thanks for helping improve this industrial automation HMI. + +## Ways to contribute + +- Fix bugs listed in GitHub Issues (preferred over `TODO.txt`) +- Improve docs, build scripts, or CI +- Add tests for models (`StepModel`, `PlcIOModel`, `RecordModel`) +- Review pull requests and leave concrete feedback + +## Development setup + +1. Install Qt 5.15+ or Qt 6 with modules: Core, Quick, Qml, QuickControls2, SerialBus, SerialPort +2. Install CMake 3.16+ and a C++17 toolchain (MSVC, MinGW, or Clang) +3. Configure and build: + +```bash +cmake -S . -B build -DCMAKE_PREFIX_PATH= +cmake --build build +``` + +4. Optional protocol helpers (Python 3.9+): + +```bash +python -m pip install -r scripts/requirements.txt +python -m pytest scripts/tests +``` + +## Pull request checklist + +- [ ] Change is focused (one concern per PR when possible) +- [ ] Build still configures with CMake +- [ ] QML/C++ naming stays consistent with existing modules +- [ ] No hardcoded secrets or machine credentials in source +- [ ] Update README / issues when behavior changes + +## Code style + +- Match surrounding C++ / QML style in the file you edit +- Prefer WeaCore logging over raw `qDebug()` for new code +- Avoid drive-by refactors unrelated to the PR + +## Reporting issues + +Use the Bug / Feature issue templates. Include Qt version, OS, Modbus mode (RTU/TCP), and steps to reproduce. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4b2327c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Parsa Pournabi (Wearily) / zero-0002 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 898d222..30c55c3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # WeaMachine +[![CI](https://github.com/zero-0002/Wea_Machine/actions/workflows/ci.yml/badge.svg)](https://github.com/zero-0002/Wea_Machine/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) + > A configurable industrial automation framework for PLC-controlled machinery, motion systems, and custom manufacturing equipment. WeaMachine is a runtime-configurable machine control platform built with C++, Qt, and QML. It combines PLC integration, servo motion control, step-based automation, recipe management, and operator-friendly machine configuration into a unified application. @@ -12,6 +15,7 @@ Instead of hardcoding machine behavior inside PLC ladder logic or application so - [Overview](#overview) - [Screenshots](#screenshots) +- [Getting Started](#getting-started) - [Core Features](#core-features) - [Runtime PLC Configuration](#runtime-plc-configuration) - [Motion Control](#motion-control) @@ -24,6 +28,8 @@ Instead of hardcoding machine behavior inside PLC ladder logic or application so - [Technology Stack](#technology-stack) - [Design Goals](#design-goals) - [Project Status](#project-status) +- [Contributing](#contributing) +- [License](#license) --- @@ -77,6 +83,40 @@ Typical use cases include: --- +## Getting Started + +### Prerequisites + +- CMake **3.16+** +- C++17 compiler (MSVC, MinGW, or Clang) +- Qt **5.15+** or Qt **6** with: Core, Quick, Qml, QuickControls2, SerialBus, SerialPort +- Python **3.9+** (optional, for Modbus protocol helpers) + +### Build + +```bash +cmake -S . -B build -DCMAKE_PREFIX_PATH= +cmake --build build +``` + +On Windows with an installed Qt kit, point `CMAKE_PREFIX_PATH` at the kit root (for example `C:/Qt/6.7.0/msvc2019_64`). + +### Run + +Launch the `InjectionMolding` target from the build directory. QML is currently loaded from the source tree via `PROJECT_SOURCE_DIR` (convenient for development). + +Default operator login is stored in QSettings under `OperatorAuth` (defaults: `admin` / `changeme`). Change these on the target machine; do not commit production credentials. + +### Protocol helpers + +```bash +python -m pip install -r scripts/requirements.txt +python scripts/protocol_map.py +PYTHONPATH=scripts python -m pytest scripts/tests -q +``` + +--- + ## Core Features ### Dynamic PLC I/O Configuration @@ -338,6 +378,20 @@ Planned improvements include: - Enhanced visualization - Machine simulation mode +Tracked work lives in [GitHub Issues](https://github.com/zero-0002/Wea_Machine/issues). Historical notes remain in `TODO.txt`. + +--- + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, PR expectations, and issue templates. + +--- + +## License + +This project is licensed under the [MIT License](LICENSE). + --- ## Author diff --git a/TODO.txt b/TODO.txt index 21030d6..11ea4b8 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,8 +1,11 @@ +# Backlog mirror +# Prefer GitHub Issues for tracking. Keep this file as a quick local checklist. + UI TODO list: 1. Complete StepItem & StepPopup such as Delay scaling, Run/Stop button, current running indicating. Also Add Profiles into the Steps (QC/QA all). 2. Manual GroupBoxes should be enable only when its servo ON and Manual mode is ON. -3. Complete Record Table and model. +3. Complete Record Table and model. [DONE: RecordModel + shot capture] 4. replace Step Start Interrupt with RADIO Button [ Step Start, Step Stop ] 5. Remove X-Axis Servo Setting Page, Y-Axis Servo Setting Page 6. Add 'General' Setting Page with these sections: [ Axes Configuration, Step Configuration ] @@ -34,9 +37,16 @@ Backend TODO list: 4. ADD Conditions Checker for IOS on Steps & Servo HOME/TPOS, .etc. 5. ADD Always ON coils while Step is Running 6. ADD Single StepItem running/pausing execution. -7. ADD RecordTable model logic. +7. ADD RecordTable model logic. [DONE: RecordModel] 8. DEL hardcoded things of Steps at Cpp/Qml. 9. ADD Alarm for when a specific Step cannot be execute or is freezing. 10. REFACTOR modbus & servomodbus classes with new logic (if needed use multi-threading) 11. ADD ModbusTCP and an environment at .ini setting for Choosing ModbusRTU or ModbusTCP. -12. ADD Counter register for when Step flow is done. \ No newline at end of file +12. ADD Counter register for when Step flow is done. + +Maintenance recently completed: +- GotoPosPanel path bindings + Go button axis enable +- Model dataChanged emits role vector (PlcIOModel / StepModel) +- CMake duplicate Qt5 link removal +- OperatorAuth via QSettings (no hardcoded production password in logic) +- LICENSE, CI, CONTRIBUTING, protocol Python helpers diff --git a/include/InjectionMolding/RecordModel.h b/include/InjectionMolding/RecordModel.h new file mode 100644 index 0000000..9ec823d --- /dev/null +++ b/include/InjectionMolding/RecordModel.h @@ -0,0 +1,118 @@ +#ifndef RECORDMODEL_H +#define RECORDMODEL_H + +#include +#include + +/*! + * \brief Persistent position samples captured during manual/auto runs. + */ +class RecordModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged) + public: + enum RecordRoles + { + NoRole = Qt::UserRole + 1, + XPosRole, + YPosRole + }; + Q_ENUM(RecordRoles) + + explicit RecordModel(QObject* parent = nullptr) + : QAbstractListModel(parent) + { + } + + static RecordModel& getInstance() + { + static RecordModel instance; + return instance; + } + + int rowCount(const QModelIndex& parent = QModelIndex()) const override + { + Q_UNUSED(parent) + return m_records.count(); + } + + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override + { + const int idx = index.row(); + if (!index.isValid() || idx < 0 || idx >= rowCount()) + return QVariant(); + + const Record& record = m_records.at(idx); + switch (role) + { + case NoRole: + return idx + 1; + case XPosRole: + return QString::number(record.xPos, 'f', 3); + case YPosRole: + return QString::number(record.yPos, 'f', 3); + default: + return QVariant(); + } + } + + QHash roleNames() const override + { + return { + {NoRole, "no"}, + {XPosRole, "xpos"}, + {YPosRole, "ypos"}, + }; + } + + Q_INVOKABLE bool addRecord(double xPos, double yPos) + { + const int insertLoc = count(); + beginInsertRows(QModelIndex(), insertLoc, insertLoc); + m_records.append(Record{xPos, yPos}); + endInsertRows(); + emit countChanged(); + return true; + } + + //! ListModel-compatible remove used by CusTableView. + Q_INVOKABLE void remove(int index) + { + if (index < 0 || index >= count()) + return; + + beginRemoveRows(QModelIndex(), index, index); + m_records.removeAt(index); + endRemoveRows(); + emit countChanged(); + + if (!isEmpty()) + emit dataChanged(this->index(0), this->index(count() - 1), QVector{NoRole}); + } + + Q_INVOKABLE void clear() + { + beginResetModel(); + m_records.clear(); + endResetModel(); + emit countChanged(); + } + + Q_INVOKABLE bool isEmpty() const { return m_records.isEmpty(); } + Q_INVOKABLE int count() const { return m_records.count(); } + + signals: + void countChanged(); + + private: + struct Record + { + double xPos = 0.0; + double yPos = 0.0; + }; + + QList m_records; +}; + +#endif // RECORDMODEL_H diff --git a/include/InjectionMolding/Registerations.h b/include/InjectionMolding/Registerations.h index 614c24c..4e84a8c 100644 --- a/include/InjectionMolding/Registerations.h +++ b/include/InjectionMolding/Registerations.h @@ -16,6 +16,7 @@ #include "stepitem.h" #include "stepmodel.h" #include "axisconfig.h" +#include "RecordModel.h" #define QT_REGISTER_METATYPE(type) \ qRegisterMetaType(#type) diff --git a/main.cpp b/main.cpp index 53d0b09..4a9e305 100644 --- a/main.cpp +++ b/main.cpp @@ -8,6 +8,7 @@ #include "InjectionMolding/Utils.h" #include "InjectionMolding/Engine.h" #include "InjectionMolding/AlarmModel.h" +#include "InjectionMolding/RecordModel.h" #include "InjectionMolding/plciomodel.h" #include "InjectionMolding/Registerations.h" @@ -75,6 +76,7 @@ int main(int argc, char* argv[]) // Engine Property Contexts engine.addEngineContextProperty(); engine.rootContext()->setContextProperty("_alarmModel", &AlarmModel::getInstance()); + engine.rootContext()->setContextProperty("_recordModel", &RecordModel::getInstance()); engine.rootContext()->setContextProperty("_plcIOModel", &PlcIOModel::getInstance()); engine.load(url); diff --git a/qml/GotoPosPanel.qml b/qml/GotoPosPanel.qml index fdbf379..cc48306 100644 --- a/qml/GotoPosPanel.qml +++ b/qml/GotoPosPanel.qml @@ -55,14 +55,11 @@ Item { title: "X-Axis Servo:" controlHeight: root.controlHeight positionSwitch.enabled: manualActive - /// FIXME: - // positionValue: servoXDevice.pathData1 / 1000 - positionValue: 1000000 / 1000 + positionValue: servoXDevice.pathData1 / 1000 positionEditBox.axisConfig: globalConfig.xAxisConfig - positionEditBox.textItem.onAccepted: - // console.log("Accepted: "); - // servoXDevice.pushPathData1(12345); - {} + positionEditBox.textItem.onAccepted: { + servoXDevice.pushPathData1(positionValue * 1000); + } } // Servo Y @@ -74,14 +71,11 @@ Item { title: "Y-Axis Servo:" controlHeight: root.controlHeight positionSwitch.enabled: manualActive - /// FIXME: - // positionValue: servoYDevice.pathData1 / 1000 - positionValue: 10000000 / 1000 + positionValue: servoYDevice.pathData1 / 1000 positionEditBox.axisConfig: globalConfig.yAxisConfig - positionEditBox.textItem.onAccepted: - // console.log("Accepted: "); - // servoXDevice.pushPathData1(12345); - {} + positionEditBox.textItem.onAccepted: { + servoYDevice.pushPathData1(positionValue * 1000); + } } // Perform Button @@ -91,7 +85,7 @@ Item { Layout.rightMargin: 10 height: controlHeight - enabled: gotoPosXControl.positionActive || gotoPosYControl.positionActive && manualActive + enabled: (gotoPosXControl.positionActive || gotoPosYControl.positionActive) && manualActive level: 2 text: "Go" diff --git a/qml/LoginPopup.qml b/qml/LoginPopup.qml index 8165383..5add09c 100644 --- a/qml/LoginPopup.qml +++ b/qml/LoginPopup.qml @@ -1,5 +1,6 @@ import QtQuick 2.12 import QtQuick.Layouts 1.12 +import Qt.labs.settings 1.0 import com.wearily.WeaQuick 1.0 as WeaQuick CusPopup { @@ -9,6 +10,15 @@ CusPopup { property bool currentFailed: false property int controlHeight: 40 + // Credentials live in QSettings (OperatorAuth). Change them on-device; + // do not ship production passwords in source. + Settings { + id: authSettings + category: "OperatorAuth" + property string username: "admin" + property string password: "changeme" + } + titleSize: 18 title: "Login" @@ -121,7 +131,7 @@ CusPopup { } function checkLoginCorrect(username, password) { - return username === "admin" && password === "qazqwe"; + return username === authSettings.username && password === authSettings.password; } function focusOnUsername() { diff --git a/qml/PanelView.qml b/qml/PanelView.qml index 7073c05..62b2627 100644 --- a/qml/PanelView.qml +++ b/qml/PanelView.qml @@ -71,36 +71,19 @@ Flickable { // gotoPosXControl.enabled: servoXDevice.availableToRun // gotoPosYControl.enabled: servoYDevice.availableToRun - submitButton.mouseArea.onPressed: { - // X Servo Goto Position - if (gotoPosPanel.gotoPosXControl.positionEditBox.enabled) - // servoXDevice.pushPathData1(gotoPosPanel.gotoPosXControl.positionValue * 1000); - // servoXDevice.pushSpeed0(speedPanel.speedXControl.value * 10); - // servoXDevice.pushRamp0(rampPanel.rampXControl.accValue); - // servoXDevice.pushDi2(true); - {} - - // Y Servo Goto Position - if (gotoPosPanel.gotoPosYControl.positionEditBox.enabled) - // servoYDevice.pushPathData1(gotoPosPanel.gotoPosYControl.positionValue * 1000); - // servoYDevice.pushSpeed0(speedPanel.speedYControl.value * 10); - // servoYDevice.pushRamp0(rampPanel.rampYControl.accValue); - // servoYDevice.pushDi2(true); - {} - } submitButton.onActivated: { - // X Servo Goto Position - if (gotoPosPanel.gotoPosXControl.positionEditBox.enabled) { - // servoXDevice.triggerCTRG(); + if (gotoPosPanel.gotoPosXControl.positionActive + && gotoPosPanel.gotoPosXControl.positionEditBox.enabled) { servoXDevice.gotoPosition(gotoPosPanel.gotoPosXControl.positionValue * 1000, - speedPanel.speedXControl.value * 10, rampPanel.rampXControl.accValue); + speedPanel.speedXControl.value * 10, + rampPanel.rampXControl.accValue); } - // Y Servo Goto Position - if (gotoPosPanel.gotoPosYControl.positionEditBox.enabled) { - // servoYDevice.triggerCTRG(); + if (gotoPosPanel.gotoPosYControl.positionActive + && gotoPosPanel.gotoPosYControl.positionEditBox.enabled) { servoYDevice.gotoPosition(gotoPosPanel.gotoPosYControl.positionValue * 1000, - speedPanel.speedYControl.value * 10, rampPanel.rampYControl.accValue); + speedPanel.speedYControl.value * 10, + rampPanel.rampYControl.accValue); } } } diff --git a/qml/RecordTable.qml b/qml/RecordTable.qml index 9218f9d..049e2c2 100644 --- a/qml/RecordTable.qml +++ b/qml/RecordTable.qml @@ -48,7 +48,7 @@ BorderRectangle { title: "Y-Axis Position (mm)" width: 220 } - model: libraryModel + model: _recordModel } RecordFooter { @@ -64,33 +64,16 @@ BorderRectangle { removeButton.enabled: tableView.currentRow > -1 cleanButton.enabled: tableView.rowCount > 0 + shotButton.enabled: recordFooter.recordButton.checked - /** Slots **/ - removeButton.onClicked: { - tableView.removeCurrent(); - } - - cleanButton.onClicked: { - tableView.removeAll(); - } - } + removeButton.onClicked: tableView.removeCurrent() + cleanButton.onClicked: tableView.removeAll() - ListModel { - id: libraryModel - ListElement { - no: "1" - xpos: "6,127.000" - ypos: "200.123" - } - ListElement { - no: "2" - xpos: "8,964.000" - ypos: "1,542.001" - } - ListElement { - no: "3" - xpos: "10,000.71" - ypos: "555.555" + shotButton.onClicked: { + // Capture current servo encoder positions (PUU → mm display units) + const xMm = servoXDevice.encoderPUU / 1000.0; + const yMm = servoYDevice.encoderPUU / 1000.0; + _recordModel.addRecord(xMm, yMm); } } } diff --git a/scripts/protocol_map.py b/scripts/protocol_map.py new file mode 100644 index 0000000..e047495 --- /dev/null +++ b/scripts/protocol_map.py @@ -0,0 +1,101 @@ +"""Delta ASD-A2 Modbus register map helpers used by WeaMachine docs/Protocol.txt.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict, Iterable, List, Optional + + +@dataclass(frozen=True) +class Register: + name: str + address: int + description: str + access: str = "RW" # R, W, RW + + +# Core registers referenced by ServoModbusDevice / docs/Protocol.txt +DELTA_ASD_A2_REGISTERS: Dict[str, Register] = { + "di_status": Register("di_status", 0x0108, "Digital input status", "R"), + "do_status": Register("do_status", 0x0109, "Digital output status", "R"), + "encoder_puu": Register("encoder_puu", 0x0000, "Encoder position PUU (low/high pair)", "R"), + "path_data_1": Register("path_data_1", 0x1702, "Internal path position P1-26/P1-27", "RW"), + "speed_data_0": Register("speed_data_0", 0x170A, "Speed command data 0", "RW"), + "ramp_data_0": Register("ramp_data_0", 0x170C, "Acc/Dec ramp data 0", "RW"), + "jog_speed": Register("jog_speed", 0x010E, "JOG speed", "RW"), + "alarms": Register("alarms", 0x0100, "Alarm code", "R"), + "di_force_enable": Register("di_force_enable", 0x030B, "Enable DI force via serial", "RW"), + "di_force_value": Register("di_force_value", 0x030C, "Forced DI bit pattern", "RW"), +} + + +SERVO_COM_DEFAULTS = { + "mode": "RTU", + "baudrate": 115200, + "bytesize": 8, + "parity": "O", + "stopbits": 1, + "x_slave": 2, + "y_slave": 3, +} + + +def get_register(name: str) -> Register: + try: + return DELTA_ASD_A2_REGISTERS[name] + except KeyError as exc: + known = ", ".join(sorted(DELTA_ASD_A2_REGISTERS)) + raise KeyError(f"Unknown register '{name}'. Known: {known}") from exc + + +def validate_register_map(registers: Optional[Iterable[Register]] = None) -> List[str]: + """Return human-readable problems found in the register map.""" + regs = list(registers) if registers is not None else list(DELTA_ASD_A2_REGISTERS.values()) + problems: List[str] = [] + seen_addresses = {} + + for reg in regs: + if reg.address < 0 or reg.address > 0xFFFF: + problems.append(f"{reg.name}: address 0x{reg.address:X} out of Modbus range") + if not reg.name: + problems.append("register with empty name") + if reg.access not in {"R", "W", "RW"}: + problems.append(f"{reg.name}: invalid access '{reg.access}'") + if reg.address in seen_addresses and seen_addresses[reg.address] != reg.name: + problems.append( + f"duplicate address 0x{reg.address:X}: {seen_addresses[reg.address]} vs {reg.name}" + ) + else: + seen_addresses[reg.address] = reg.name + + return problems + + +def format_register_table(registers: Optional[Dict[str, Register]] = None) -> str: + regs = registers or DELTA_ASD_A2_REGISTERS + lines = ["Name | Address | Access | Description", "---- | ------- | ------ | -----------"] + for key in sorted(regs): + reg = regs[key] + lines.append(f"{reg.name} | 0x{reg.address:04X} | {reg.access} | {reg.description}") + return "\n".join(lines) + + +def main() -> int: + problems = validate_register_map() + print(format_register_table()) + print() + if problems: + print("Validation FAILED:") + for problem in problems: + print(f" - {problem}") + return 1 + print("Validation OK: register map is consistent.") + print( + "Default COM: {mode} {baudrate} {bytesize}{parity}{stopbits}, " + "slaves X={x_slave} Y={y_slave}".format(**SERVO_COM_DEFAULTS) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000..b197d32 --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1 @@ +pytest>=7.0 diff --git a/scripts/tests/__init__.py b/scripts/tests/__init__.py new file mode 100644 index 0000000..bbc7b9f --- /dev/null +++ b/scripts/tests/__init__.py @@ -0,0 +1 @@ +# Empty package marker so pytest can import sibling modules via sys.path in CI. diff --git a/scripts/tests/test_protocol_map.py b/scripts/tests/test_protocol_map.py new file mode 100644 index 0000000..3aeab2b --- /dev/null +++ b/scripts/tests/test_protocol_map.py @@ -0,0 +1,34 @@ +from protocol_map import ( + DELTA_ASD_A2_REGISTERS, + Register, + format_register_table, + get_register, + validate_register_map, +) + + +def test_core_registers_present(): + for name in ("encoder_puu", "path_data_1", "di_force_enable", "alarms"): + reg = get_register(name) + assert reg.name == name + assert 0 <= reg.address <= 0xFFFF + + +def test_validate_default_map_ok(): + assert validate_register_map() == [] + + +def test_validate_detects_duplicate_address(): + regs = [ + Register("a", 0x10, "first"), + Register("b", 0x10, "second"), + ] + problems = validate_register_map(regs) + assert any("duplicate" in p for p in problems) + + +def test_format_register_table_contains_header(): + table = format_register_table() + assert "Name | Address" in table + assert "path_data_1" in table + assert len(DELTA_ASD_A2_REGISTERS) >= 8 diff --git a/src/plciomodel.cpp b/src/plciomodel.cpp index 18502b2..7e0c1ff 100644 --- a/src/plciomodel.cpp +++ b/src/plciomodel.cpp @@ -2,6 +2,7 @@ #include #include +#include PlcIOModel::PlcIOModel(QObject* parent) : QAbstractListModel(parent) @@ -143,8 +144,7 @@ bool PlcIOModel::setData(const QModelIndex& index, const QVariant& value, int ro }; - // FIXME: Check this correction - emit dataChanged(index, index); + emit dataChanged(index, index, QVector{role}); return true; } diff --git a/src/stepmodel.cpp b/src/stepmodel.cpp index 41bb607..4fef340 100644 --- a/src/stepmodel.cpp +++ b/src/stepmodel.cpp @@ -5,8 +5,9 @@ #include "../include/InjectionMolding/servomodbusdevice.h" #include -#include +#include #include +#include StepModel::StepModel(QObject* parent) @@ -116,8 +117,7 @@ bool StepModel::setData(const QModelIndex& index, const QVariant& value, int rol auto* item = m_items[idx]; setData(item, value, role); - // FIXME: Check this correction - emit dataChanged(index, index); + emit dataChanged(index, index, QVector{role}); return true; }