diff --git a/.github/workflows/core-baseStation.yml b/.github/workflows/core-baseStation.yml index 59fb54df85..51fc455eeb 100644 --- a/.github/workflows/core-baseStation.yml +++ b/.github/workflows/core-baseStation.yml @@ -1,47 +1,22 @@ -# This is a basic workflow to help you get started with Actions - name: CI - Core - Base Station -# Controls when the workflow will run on: - # Triggers the workflow on push or pull request events but only for the main branch - push: - branches: [ "**" ] - paths: - - "software/core/oqm-core-base-station/**" - - ".github/workflows/core-baseStation.yml" - - ".github/workflows/wf-gradleBuild.yaml" - - ".github/workflows/wf-gradleUnitTest.yaml" - - ".github/workflows/wf-gradleQuarkusIntTest.yaml" pull_request: - branches: [ "**" ] paths: - "software/core/oqm-core-base-station/**" - ".github/workflows/core-baseStation.yml" - - ".github/workflows/wf-gradleBuild.yaml" - - ".github/workflows/wf-gradleUnitTest.yaml" - - ".github/workflows/wf-gradleQuarkusIntTest.yaml" workflow_call: - # Allows you to run this workflow manually from the Actions tab workflow_dispatch: -defaults: - run: - working-directory: "software/core/oqm-core-base-station" -# A workflow run is made up of one or more jobs that can run sequentially or in parallel + +concurrency: + group: ci-core-base-station-${{ github.ref }} + cancel-in-progress: true + jobs: - build: - uses: ./.github/workflows/wf-gradleBuild.yaml - with: - path: "software/core/oqm-core-base-station" - unitTest: - uses: ./.github/workflows/wf-gradleUnitTest.yaml - with: - path: "software/core/oqm-core-base-station" - intTest: - uses: ./.github/workflows/wf-gradleQuarkusIntTest.yaml - strategy: - matrix: - containerBased: [ false ] # TODO:: enable true + CI-Pipeline: + uses: Epic-Breakfast-Productions/ebp-ci/.github/workflows/quarkus-ci-pipeline.yml@main with: path: "software/core/oqm-core-base-station" - containerBased: ${{ matrix.containerBased }} + java-version: "25" + run-int-tests: true + container-based-int-tests: false diff --git a/.gitignore b/.gitignore index b5e691768d..94c12de109 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ hs_err_pid* bin/ target/ +.gradle/ \ No newline at end of file diff --git a/README.md b/README.md index 1738a41779..e13a32e47a 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,11 @@ # Open QuarterMaster +**Inventory without a catch, and all the hooks** + ![GitHub commit activity](https://img.shields.io/github/commit-activity/m/Epic-Breakfast-Productions/OpenQuarterMaster) ![GitHub all releases](https://img.shields.io/github/downloads/Epic-Breakfast-Productions/OpenQuarterMaster/total) -![Core API](https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/actions/workflows/core-api.yml/badge.svg) [![Code Triage Open Source Helpers](https://www.codetriage.com/epic-breakfast-productions/openquartermaster/badges/users.svg)](https://www.codetriage.com/epic-breakfast-productions/openquartermaster) [//]: # (![Station Captain](https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/actions/workflows/stationCaptain.yml/badge.svg)) @@ -16,33 +17,57 @@ [![All Contributors](https://img.shields.io/badge/all_contributors-22-orange.svg?style=flat-square)](#contributors-) -**Inventory without a catch, and all the hooks** +
+🛠 CI Status 🛠 + +## [Core](./software/core) + +| Service | Status | +|-------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| +| [Core API](./software/core/oqm-core-api) | ![Core API](https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/actions/workflows/core-api.yml/badge.svg) | +| [Base Station](./software/core/oqm-core-base-station) | ![Base Station](https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/actions/workflows/core-baseStation.yml/badge.svg) | + +## [Plugins](./software/plugins) + +| Service | Status | +|-----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| [External Item Search](./software/plugins/external-item-search) | ![External Item Search](https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/actions/workflows/plugin-extItemSearch.yml/badge.svg) | +| [Storagotchi](./software/plugins/storagotchi) | ![Storagotchi](https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/actions/workflows/plugin-storagotchi.yml/badge.svg) | + +
+ Open Quartermaster is an open source inventory management system, designed to be simple to use yet powerful and extendable. The last inventory management system you will ever need! -We are very much in development still, so check back often! We are also accepting any and all assistance, so feel free to report issues or feature requests, as well as pull requests! Additionally, feel free to ask questions in the [Discussions](https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/discussions) or just hang out with us on our [Discord](https://discord.gg/cpcVh6SyNn) +We are very much in development still, so check back often! We are also accepting any and all assistance, so feel free to report issues or feature requests, as well as pull requests! Additionally, feel free to ask questions in +the [Discussions](https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/discussions) or just hang out with us on our [Discord](https://discord.gg/cpcVh6SyNn) ## Quick Links - - For a quick start running on your own computer, check out [Single Host Deployment](deployment/Single%20Host) - - To see all the ways you can deploy OQM for yourself, see [Deployment](deployment/) - - For information on the overall system, see the [software](software/) directory. +- For a quick start running on your own computer, check out [Single Host Deployment](deployment/Single%20Host) +- To see all the ways you can deploy OQM for yourself, see [Deployment](deployment/) +- For information on the overall system, see the [software](software/) directory. ## How it works -How we accomplish the goal of being the only inventory management system you could ever need is through our modular design. The main component of Open QuarterMaster is the [Core API](software/core/oqm-core-api). Think of this as the central hub and core functionality of the system. It handles all the generic inventory management tasks; what is stored where, and facts about what is stored. This central component is designed to be, on the whole, generic and accessible. We also have a frontend for the core API called the [Base Station](software/core/oqm-core-base-station), which lets you have direct and easy to navigate access to your inventory. +How we accomplish the goal of being the only inventory management system you could ever need is through our modular design. The main component of Open QuarterMaster is the [Core API](software/core/oqm-core-api). Think of this as the central +hub and core functionality of the system. It handles all the generic inventory management tasks; what is stored where, and facts about what is stored. This central component is designed to be, on the whole, generic and accessible. We also +have a frontend for the core API called the [Base Station](software/core/oqm-core-base-station), which lets you have direct and easy to navigate access to your inventory. -To cover specific use-cases, we have what we call [Plugins](software/plugins). These are components that extend the functionality of the basic inventory management, and fill additional needs with their own capabilities. Examples could include Smart Refrigerator integrations, a system for interacting with physical storage mediums, Point of Sale Systems, Workflow management.. the list is endless. You could even create your own! +To cover specific use-cases, we have what we call [Plugins](software/plugins). These are components that extend the functionality of the basic inventory management, and fill additional needs with their own capabilities. Examples could +include Smart Refrigerator integrations, a system for interacting with physical storage mediums, Point of Sale Systems, Workflow management.. the list is endless. You could even create your own! -In the theme of flexibility, the system is designed to be run in many different environments. It is just as home on the cloud as well as something as small as a [Raspberry Pi](https://www.raspberrypi.com/). This is accomplished using containers, segmenting each software component, ensuring flexibility and ease of management. +In the theme of flexibility, the system is designed to be run in many different environments. It is just as home on the cloud as well as something as small as a [Raspberry Pi](https://www.raspberrypi.com/). This is accomplished using +containers, segmenting each software component, ensuring flexibility and ease of management. To get started on your own hardware, please see [Single Host Deployment](deployment/Single%20Host) -For more information on the overall system, see the [software](software/) directory. +For more information on the overall system, see the [software](software/) directory. ## On Privacy -Being an open initiative, we take great care to ensure you are in control of your own data. None of the software we include here phones home at all, with the brief exception of Station Captain, which looks to this Git repository for installations and updates. If you have a simple setup on your own hardware, you can expect your data to stay with you, and not transmitted anywhere by the software we include here. +Being an open initiative, we take great care to ensure you are in control of your own data. None of the software we include here phones home at all, with the brief exception of Station Captain, which looks to this Git repository for +installations and updates. If you have a simple setup on your own hardware, you can expect your data to stay with you, and not transmitted anywhere by the software we include here. ## Contributors ✨ diff --git a/hardware/speed-scanner/OQM-Scanner-Community-Guide.md b/hardware/speed-scanner/OQM-Scanner-Community-Guide.md new file mode 100644 index 0000000000..13e4e9d187 --- /dev/null +++ b/hardware/speed-scanner/OQM-Scanner-Community-Guide.md @@ -0,0 +1,142 @@ +# Community Guide - OQM Fast Transaction Scanner + +This project provides a hardware-based solution to automate inventory management for the **Open Quarter Master (OQM)** system. It replaces manual web-browser data entry with a standalone physical device that connects via Wi-Fi for real-time updates. + +## Table of Contents +1. [Bill of Materials (BOM)](#bill-of-materials-bom) + - [Compatibility Warning](#compatibility-warning) + - [Required Hardware Components](#required-hardware-components) +2. [Hardware Assembly & Pinout](#hardware-assembly--pinout) + - [Master Wiring Table](#master-wiring-table) +3. [Firmware Files Structure](#firmware-files-structure) +4. [SD Card Configuration](#sd-card-configuration) +5. [Security Logic](#security-logic) +6. [Software Installation & Setup](#software-installation--setup) + - [Environment Setup](#environment-setup) + - [Required Libraries](#required-libraries) + - [Flashing the Firmware](#flashing-the-firmware) +7. [Interface Guide](#interface-guide) + - [Startup & Main Menu](#startup--main-menu) + - [Quick Mode](#quick-mode) + - [Details Mode](#details-mode) + +--- + +## 1. Bill of Materials (BOM) + +### Compatibility Warning +This guide and the accompanying firmware are designed **strictly** for the components listed below. The pin assignments are hardcoded for this specific hardware stack. Using different components may lead to pinout conflicts or driver issues, as the team has not tested alternative hardware configurations. + +### Required Hardware Components + +| Item | Description | Purpose | Source / Link | Price (Est. USD) | +| :--- | :--- | :--- | :--- | :--- | +| **Adafruit Feather ESP32 V2** | Microcontroller with 8MB Flash & Wi-Fi | The "brain" of the device, handling API calls and security. | [Adafruit Store](https://www.adafruit.com/product/5400) | $19.95 | +| **TFT FeatherWing 3.5"** | 480x320 Color Touchscreen (V2) | Visual interface for database and block selection. | [Adafruit Store](https://www.adafruit.com/product/3651) | $39.95 | +| **ATOMIC QR-Code Scanner** | 1D/2D scanning module | High-speed data acquisition of product IDs. | [M5Stack Store](https://shop.m5stack.com/products/atom-barcode-scanner-base) | $16.95 | +| **MicroSD Card** | Any standard MicroSD card (FAT32) | Stores `sdsetup.json` for Wi-Fi and API configuration. | Generic | ~$8.00 | +| **Jumper Cables (4-pin)** | Female to Female / Female to Male | Connects the ATOMIC scanner to the Feather Pins 20/22. | [DigiKey](https://www.google.com/search?q=https://www.digikey.com/en/products/detail/sparkfun-electronics/CAB-22726/18066531) / [Amazon](https://www.google.com/search?q=https://www.amazon.com/SparkFun-Qwiic-Cable-Female-Jumper/dp/B07S1V8Z7F) | ~$2.00 | +| **Standard Hook-up Wires** | Basic solid or stranded wires | Necessary for custom soldering of headers and power lines. | [Adafruit](https://www.adafruit.com/product/3894) / [Amazon](https://www.amazon.com/Qwiic-Cable-Female-Jumper-4-pin/dp/B0992PHLBC) | ~$2.00 | +| **TOTAL** | | | **Approximate Cost per Unit** | **~$88.85** | + +### Note on Power Supply (Battery) +Our current prototype is powered via the USB-C port of the Feather ESP32 for development and testing stability. While the system is designed for portability, a 3.7V Li-Po battery is not included in this core build. We recommend adding one as a future evolution to ensure full warehouse mobility. + +--- + +## 2. Hardware Assembly & Pinout + +To ensure the firmware initializes correctly, you must follow this specific hardware configuration. These mappings are defined in `barcode_scanner.ino` and `functions.ino`. + +### Master Wiring Table + +| Peripheral | Signal Name | Pin on Feather ESP32 V2 | Description | +| :--- | :--- | :--- | :--- | +| **ATOMIC Scanner** | UART RX | **Pin 20** | Data sent from Scanner to the ESP32. | +| **ATOMIC Scanner** | UART TX | **Pin 22** | Commands sent from ESP32 to the Scanner. | +| **TFT FeatherWing** | TFT_CS | **Pin 15** | Chip Select for the display logic. | +| **TFT FeatherWing** | TFT_DC | **Pin 33** | Data/Command toggle for the screen. | +| **TFT FeatherWing** | TFT_RST | **Pin 32** | Hardware Reset for the display. | +| **Integrated SD Slot** | SD_CS | **Pin 14** | Chip Select for the MicroSD card. | +| **All Peripherals** | VCC | **3V** | Power supply (Red wire). | +| **All Peripherals** | GND | **GND** | Common ground (Black wire). | + +> [!IMPORTANT] +> **Soldering Requirement:** You **MUST solder** the headers to connect the TFT FeatherWing to the ESP32 V2 board. The high-speed SPI bus (used for the TFT and SD card) will fail if connections are loose. Friction-fit or "plug-and-play" attempts will cause the screen to fail during initialization. + +--- + +## 3. Firmware Files Structure + +To compile the project, you need to include the following files in your Arduino project folder: + +* **`oqm_scanner.ino`**: The main entry point. It initializes the hardware (TFT, Scanner, WiFi) and runs the primary loop. Also contains http functions +* **`color.h`**: Contains default color definitions. +* **`scannerUI.ino`**: Handles all graphical rendering for the Sage Green interface, including the "Quick" and "Detail" mode screens. + +--- + +## 4. SD Card Configuration + +The scanner is designed to be "zero-code" for configuration. It looks for a file named `sdsetup.json` on the MicroSD card to set up the network and security. + +1. Format your MicroSD card to **FAT32**. +2. Copy the provided `sdsetup.json` file to the root directory. +3. Fill in your credentials: + * **ssid / password**: Your local Wi-Fi. + * **oqm-address**: The IP of your OQM Basestation. + * **oqm-user / oqm-secret**: Your Keycloak client credentials. + +--- + +## 5. Security Logic + +To ensure professional-grade reliability, the firmware includes the following automated safety features: + +* **JWT Authentication**: The system handles secure Keycloak authentication. It automatically calculates the token's expiration and performs a refresh when 75% of its lifespan has passed, ensuring the scanner never logs out during a shift. +* **Scan Buffer**: A mandatory 5-second delay is enforced between successful scans. This prevents accidental duplicate entries and ensures the OQM server has enough time to process and acknowledge the previous transaction. +* **Modularity**: By using **Pin 14** for the SD card, the system remains modular, allowing users to update Wi-Fi or API settings without ever touching the source code. + +--- + +## 6. Software Installation & Setup + +To compile and flash the firmware, follow these steps to prepare your development environment. + +### Environment Setup +* **IDE**: Download and install **Arduino IDE** (version 2.3.8 or higher). +* **Board Manager**: Install the **ESP32 by Espressif** board package via the Boards Manager. +* **Selection**: Select **Adafruit ESP32 Feather V2** as your target board. + +### Required Libraries +Open the Library Manager and install the following: +* **Adafruit GFX & Adafruit HX8357**: Essential for driving the 3.5" TFT display. +* **M5UnitQRCode**: Used to interface with the ATOMIC barcode module. +* **ArduinoJson**: Required for parsing `sdsetup.json` and handling OQM API responses. +* **WiFi & HTTPClient**: Standard ESP32 libraries for network communication. + +### Flashing the Firmware +1. Connect the Feather ESP32 V2 to your computer via a high-quality USB-C cable. +2. Open `barcode_scanner.ino` (this is the main file that links `functions.ino` and `scannerUI.ino`). +3. Select the correct COM port and click **Upload**. + +--- + +## 7. Interface Guide + +The UI is designed for high visibility in warehouse environments, featuring a clean white interface with **Sage Green** interactive elements. + +### Startup & Main Menu +* **Initialization**: Upon power-up, the device automatically mounts the SD card and loads credentials from `sdsetup.json`. +* **Database Selection**: Tap the "Choose" button next to "DB" to toggle through available OQM databases. +* **Block Selection**: Tap "Choose" next to "Block" to define the specific storage area for your transactions. + +### Quick Mode +* **Purpose**: Optimized for fast, repetitive stock changes. +* **Scanning**: Point the scanner at a barcode. The product name, ID, and current count will appear under the "Last Item" header. +* **Interaction**: Use the large **[ + ]** and **[ - ]** buttons to instantly increment or decrement stock levels in the OQM system. + +### Details Mode +* **Navigation**: Tap the **[ -> ]** arrow on the main screen to enter Details Mode. +* **Options**: Select from five transaction types: **Add**, **Sub**, **Checkout**, **Checkin**, or **Set**. +* **Confirmation**: Adjust the quantity and tap **"Submit"** to trigger the API call. diff --git a/hardware/speed-scanner/README.md b/hardware/speed-scanner/README.md index 9352703f0a..a34e99b33c 100644 --- a/hardware/speed-scanner/README.md +++ b/hardware/speed-scanner/README.md @@ -4,5 +4,4 @@ This device's goal is to be a low-cost scanner to facilitate fast transactions f Usecases being to quickly read in groceries brought in from the store, etc. Be a convenient tool and make the ssytem truly streamlined to use. -TODO #1011 diff --git a/hardware/speed-scanner/TouchScannerTest.ino b/hardware/speed-scanner/TouchScannerTest.ino new file mode 100644 index 0000000000..66f0d09f4c --- /dev/null +++ b/hardware/speed-scanner/TouchScannerTest.ino @@ -0,0 +1,212 @@ + +// test for: touch ui, command barcodes, and axis corrected touch + +#include +#include +#include +#include "Adafruit_GFX.h" +#include "Adafruit_HX8357.h" +#include "Adafruit_TSC2007.h" + +//touch pin definitions +#define TFT_CS 15 +#define TFT_DC 33 +#define TFT_RST -1 + +Adafruit_HX8357 tft = Adafruit_HX8357(TFT_CS, TFT_DC, TFT_RST); +Adafruit_TSC2007 touch; + +//scanner pin definitions +#define FEATHER_RX 7 // tx on scanner - yellow +#define FEATHER_TX 8 // rx on scanner - blue +#define STEMMA_PWR 2 // stemma power for touch screen + +bool isAddMode = true; +std::map addCounts; +std::map removeCounts; + +//command barcode definitions +const String CMD_IN = "CMD_MODE_IN"; +const String CMD_OUT = "CMD_MODE_OUT"; + +//ui helper functions +void drawButtons() { + tft.setTextSize(3); + + if (isAddMode) { + tft.fillRect(10, 50, 225, 140, HX8357_GREEN); + tft.fillRect(245, 50, 225, 140, 0x8000); + + tft.setTextColor(HX8357_BLACK); + tft.setCursor(50, 105); + tft.print("CHECK IN"); + + tft.setTextColor(HX8357_WHITE); + tft.setCursor(275, 105); + tft.print("CHECK OUT"); + } else { + tft.fillRect(10, 50, 225, 140, 0x0400); + tft.fillRect(245, 50, 225, 140, HX8357_RED); + + tft.setTextColor(HX8357_WHITE); + tft.setCursor(50, 105); + tft.print("CHECK IN"); + + tft.setTextColor(HX8357_BLACK); + tft.setCursor(275, 105); + tft.print("CHECK OUT"); + } +} + +void drawUI() { + tft.fillScreen(HX8357_BLACK); + + tft.fillRect(0, 0, 480, 40, HX8357_BLUE); + tft.setTextColor(HX8357_WHITE); + tft.setTextSize(2); + tft.setCursor(10, 12); + tft.print("Open QuarterMaster | Status: Local"); + + drawButtons(); + + tft.drawRect(0, 210, 480, 110, HX8357_WHITE); + tft.setCursor(10, 220); + tft.setTextColor(HX8357_YELLOW); + tft.setTextSize(2); + tft.print("System Ready. Scan to begin..."); +} + +void updateLog(String barcode) { + tft.fillRect(2, 212, 476, 106, HX8357_BLACK); + tft.setTextSize(3); + tft.setCursor(10, 230); + + if (isAddMode) { + addCounts[barcode]++; + tft.setTextColor(HX8357_GREEN); + tft.print("ADDED (+"); + tft.print(addCounts[barcode]); + tft.print("): "); + } else { + removeCounts[barcode]++; + tft.setTextColor(HX8357_RED); + tft.print("REMOVED (-"); + tft.print(removeCounts[barcode]); + tft.print("): "); + } + + tft.setTextColor(HX8357_WHITE); + tft.setCursor(10, 270); + tft.setTextSize(4); + tft.print(barcode); +} + +void showModeSwitchMessage(String modeName, uint16_t color) { + tft.fillRect(2, 212, 476, 106, HX8357_BLACK); + tft.setTextSize(3); + tft.setTextColor(color); + tft.setCursor(10, 240); + tft.print(">> SWITCHED TO "); + tft.print(modeName); + tft.print(" <<"); +} + +void setup() { + Serial.begin(115200); + + //turn on stemma power + pinMode(STEMMA_PWR, OUTPUT); + digitalWrite(STEMMA_PWR, HIGH); + delay(500); + + Serial.println("--- ESP32 QuarterMaster Terminal Start ---"); + + // initialize the display + tft.begin(HX8357D); + tft.setRotation(1); + drawUI(); + + //initialize touch screen + if (!touch.begin()) { + Serial.println("Touch Controller Failed!"); + } else { + Serial.println("Touch Controller Initialized."); + } + + //initialize scanner connection + Serial1.begin(115200, SERIAL_8N1, FEATHER_RX, FEATHER_TX); + Serial.println("Scanner Connection Opened."); +} + +void loop() { + //check touch screen first + TS_Point p = touch.getPoint(); + + // check if pressure is above 100 + if (p.z > 100) { + + //debug to print where tap is taking place + Serial.printf("TAP! Raw X: %d | Raw Y: %d | Pressure: %d\n", p.x, p.y, p.z); + + if (p.y < 2000) { + if (!isAddMode) { + isAddMode = true; + drawButtons(); + Serial.println("Mode Switched: CHECK IN (Via Touch)"); + } + } else { + if (isAddMode) { + isAddMode = false; + drawButtons(); + Serial.println("Mode Switched: CHECK OUT (Via Touch)"); + } + } + delay(300); //delay to stop repeated taps + } + + //check scanner + if (Serial1.available()) { + String data = ""; + + // read characters until new line + while (Serial1.available()) { + char c = Serial1.read(); + if (c != '\r' && c != '\n') { + data += c; + } + delay(2); + } + + if (data.length() > 0) { + if (data == CMD_IN) { + isAddMode = true; + drawButtons(); + showModeSwitchMessage("CHECK IN", HX8357_GREEN); + Serial.println(">>> MODE SWITCHED: CHECK IN (Via Barcode)"); + delay(1500); + tft.fillRect(2, 212, 476, 106, HX8357_BLACK); + } + else if (data == CMD_OUT) { + isAddMode = false; + drawButtons(); + showModeSwitchMessage("CHECK OUT", HX8357_RED); + Serial.println(">>> MODE SWITCHED: CHECK OUT (Via Barcode)"); + delay(1500); + tft.fillRect(2, 212, 476, 106, HX8357_BLACK); + } + else { + updateLog(data); + + Serial.println("-------------------------"); + if (isAddMode) { + Serial.printf("[CHECK IN] Scanned: %s | Total Adds: %d\n", data.c_str(), addCounts[data]); + } else { + Serial.printf("[CHECK OUT] Scanned: %s | Total Removes: %d\n", data.c_str(), removeCounts[data]); + } + Serial.println("-------------------------"); + delay(1000); + } + } + } + delay(10); +} \ No newline at end of file diff --git a/hardware/speed-scanner/barcodeScanTest.ino b/hardware/speed-scanner/barcodeScanTest.ino new file mode 100644 index 0000000000..506011cf74 --- /dev/null +++ b/hardware/speed-scanner/barcodeScanTest.ino @@ -0,0 +1,114 @@ +// Jase Colino +// QR / Barcode scanning device using ESP32, Atomic QRCode2, TFT FeatherWing +// this program tests the scanning of a barcode or QR code and will eventually display the barcode reading on the TFT screen + +#include +#include +#include "Adafruit_GFX.h" +#include "Adafruit_HX8357.h" +#include "M5UnitQRCode.h" + +// screen pin definitions for esp32 +#define TFT_CS 15 +#define TFT_DC 33 +#define TFT_RST 32 + +Adafruit_HX8357 tft = Adafruit_HX8357(TFT_CS, TFT_DC, TFT_RST); + +// pin definitions for scanner +#define FEATHER_RX 20 +#define FEATHER_TX 22 + +M5UnitQRCodeUART qrcode; + +#define UART_AUTO_SCAN_MODE + +void setup() { + Serial.begin(115200); + delay(2000); + Serial.println("ESP32 Feather V2 Atomic QR Scanner Start"); + + // initialize tft screen --- *TFT MUST BE SOLDERED OTHERWISE DISPLAY WILL NOT WORK* + tft.begin(HX8357D); // display type + tft.setRotation(1); + tft.fillScreen(HX8357_BLACK); + + tft.setTextColor(HX8357_WHITE); + tft.setTextSize(2); + tft.setCursor(10, 10); + + // scanner initialization + // must use 115200 because of wire limitations + if (!qrcode.begin(&Serial1, 115200, FEATHER_RX, FEATHER_TX)) { + + // if 115200 somehow fails uses 9600 + if (!qrcode.begin(&Serial1, 9600, FEATHER_RX, FEATHER_TX)) { + Serial.println("Scanner Failed"); + + tft.fillScreen(HX8357_RED); + tft.setCursor(10, 10); + tft.setTextColor(HX8357_WHITE); + tft.println("Scanner Failed"); + tft.setCursor(10, 40); + + while (1) delay(10); // stop safely -- no crashing + } + } + + Serial.println("Scanner Success"); + + // update screen to ready state + tft.fillScreen(HX8357_BLACK); + tft.setCursor(10, 10); + tft.setTextColor(HX8357_GREEN); + tft.println("Scanner Ready"); + tft.setTextColor(HX8357_WHITE); + tft.setCursor(10, 40); + tft.println("Waiting for Barcode"); + +// scan mode +#ifdef UART_AUTO_SCAN_MODE + Serial.println("Mode: Auto Scan"); + qrcode.setTriggerMode(AUTO_SCAN_MODE); +#else + Serial.println("Mode: Manual Scan"); + qrcode.setTriggerMode(MANUAL_SCAN_MODE); +#endif +} + +void loop() { + if (qrcode.available()) { + String data = qrcode.getDecodeData(); + + Serial.print("Scan Result: "); + Serial.println(data); + Serial.printf("Length: %d\n", data.length()); + Serial.println("-------------------------"); + + // print to tft screen *not sure if this works as we cant test since the esp is not soldered to tft* + tft.fillScreen(HX8357_BLACK); + + tft.setCursor(10, 10); + tft.setTextColor(HX8357_CYAN); + tft.setTextSize(2); + tft.println("Result:"); + + tft.setCursor(10, 40); + tft.setTextColor(HX8357_WHITE); + tft.setTextSize(3); + tft.println(data); + } + + +// auto scan +#ifndef UART_AUTO_SCAN_MODE + static unsigned long lastTrigger = 0; + if (millis() - lastTrigger > 5000) { + qrcode.setDecodeTrigger(true); + lastTrigger = millis(); + Serial.println("Manual Trigger Sent..."); + } +#endif + + delay(10); +} \ No newline at end of file diff --git a/hardware/speed-scanner/color.h b/hardware/speed-scanner/color.h new file mode 100644 index 0000000000..4904c4c80c --- /dev/null +++ b/hardware/speed-scanner/color.h @@ -0,0 +1,12 @@ +#pragma once + +#define COLOR_BG 0xFFFF +#define COLOR_HEADER 0x5D8A +#define COLOR_CARD 0xFFFF +#define COLOR_TEXT 0x2104 +#define COLOR_BTN 0x5D8A +#define COLOR_BTN_NEG 0xC618 +#define COLOR_DIVIDER 0xC618 +#define COLOR_SELECTED 0x3C67 +#define COLOR_WHITE 0xFFFF +#define COLOR_SUBMIT 0x0340 \ No newline at end of file diff --git a/hardware/speed-scanner/oqm_scanner.ino b/hardware/speed-scanner/oqm_scanner.ino new file mode 100644 index 0000000000..99aee9abb1 --- /dev/null +++ b/hardware/speed-scanner/oqm_scanner.ino @@ -0,0 +1,857 @@ +// ============================================================ +// OQM Barcode Scanner — Main Sketch +// Hardware: Adafruit ESP32 Feather V2 + HX8357 TFT + M5 QRCode2 +// ============================================================ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "Adafruit_GFX.h" +#include "Adafruit_HX8357.h" +#include "color.h" +#include "M5UnitQRCode.h" + +// ── TFT ────────────────────────────────────────────────────── +#define TFT_CS 15 +#define TFT_DC 33 +#define TFT_RST 32 +Adafruit_HX8357 tft = Adafruit_HX8357(TFT_CS, TFT_DC, TFT_RST); +Adafruit_TSC2007 touch; + +// ── Scanner UART ───────────────────────────────────────────── +// M5 QRCode2 connected to ESP32 Serial1 (GPIO 7 RX, 8 TX) +#define FEATHER_RX 7 +#define FEATHER_TX 8 +M5UnitQRCodeUART qrcode; +#define UART_AUTO_SCAN_MODE + +// ── Screen state ───────────────────────────────────────────── +enum Screen { SCREEN_SETTINGS, SCREEN_QUICK, SCREEN_DETAIL }; +Screen currentScreen = SCREEN_SETTINGS; +Screen previousScreen = SCREEN_SETTINGS; + +// ── UI state (shared with scannerUI.ino via extern) ────────── +String currentBarcode = ""; +String currentItemName = "None"; +int currentItemCount = 0; +int adjustedCount = 0; + +// String actionNames[] = {"Add", "Sub", "Out", "In", "Set"}; +String actionNames[] = {"Add", "Sub", "Out", "", "Set"}; +int selectedAction = 0; // 0-4 → transaction types 1-5 + +bool showingDBList = false; +bool showingBlockList = false; + +// DB list overlay +String dbList[10]; +int dbCount = 0; + +// Block list overlay — label + ID kept in sync +String blockList[10]; +String blockListIDs[10]; +int blockCount = 0; + +// ── API / session state ─────────────────────────────────────── +String db_name = ""; +String storage_block = ""; // active block ID +String block_name = ""; // active block label (display) +String item_ID = ""; +String item_name = ""; + +// Quick-mode add/subtract toggle (true = add) +bool quickModeAdd = true; + +// Pending scan flag — set by UART ISR context, consumed by loop() +bool newScanPending = false; +bool apiInFlight = false; + +// ── Credentials / endpoints ─────────────────────────────────── +struct MyProjectSettings { + String my_ssid; + String my_pass; + String remote_host; + const int remote_port = 443; + const char* db_path = "/core/api/api/v1/inventory/manage/db"; + const char* def_path = "/core/api/api/v1/db/"; + const char* stor_path = "/inventory/storage-block"; + const char* get_path = "/inventory/item?identifier="; + const char* auth_path = "/infra/keycloak/realms/oqm/protocol/openid-connect/token"; + String auth_user; + String auth_pass; +} settings; + +// ── JSON documents ──────────────────────────────────────────── +JsonDocument authdoc; +JsonDocument dbdoc; +JsonDocument stblockdoc; +JsonDocument getdoc; +JsonDocument secretdoc; + +// ── Timing ──────────────────────────────────────────────────── +unsigned long time_at_auth = 0; +unsigned long expiry = 0; +unsigned long lastTouchTime = 0; + +// ── SD ──────────────────────────────────────────────────────── +SdFat32 SD; +bool gotSD = false; + +// ── UART buffer ─────────────────────────────────────────────── +String serialBuffer = ""; + +// ── Forward declarations ────────────────────────────────────── +void requestAuth(); +void GetDB(); +void GetStorageBlocks(); +void GetItem(String identifier); +void UpdateCount(String stblock, String itemID, int transaction, int value); +void HandleNewScan(); +void processTouch(int x, int y); +void populateDBList(); +void populateBlockList(bool filteredByItem); +void handleDBListTouch(int x, int y); +void handleBlockListTouch(int x, int y); +void onDBSelected(int index); +void onBlockSelected(int index); +void refreshQuickCard(); +void refreshDetailCard(); +void drawSettingsScreen(); +void drawQuickScreen(); +void drawDetailScreen(); +void drawDBListOverlay(); +void drawBlockListOverlay(); +void drawButton(int x, int y, int w, int h, String label, uint16_t bgColor); +void drawRadioButton(int x, int y, String label, bool selected); + +// ============================================================ +// SETUP +// ============================================================ +void setup() { + Serial.begin(115200); + // M5 QRCode2 UART + // Scanner initialization + if (!qrcode.begin(&Serial1, 115200, FEATHER_RX, FEATHER_TX)) { + if (!qrcode.begin(&Serial1, 9600, FEATHER_RX, FEATHER_TX)) { + // removed while(1) so it keeps going even without scanner + } + } + Serial.println("Scanner Success"); + + #ifdef UART_AUTO_SCAN_MODE + Serial.println("Mode: Auto Scan"); + qrcode.setTriggerMode(AUTO_SCAN_MODE); + #else + Serial.println("Mode: Manual Scan"); + qrcode.setTriggerMode(MANUAL_SCAN_MODE); + #endif + + // Touch controller + Wire.begin(); + if (!touch.begin()) { + Serial.println("TSC2007 not found!"); + } + + const int SD_CS = 14; + pinMode(SD_CS, OUTPUT); + digitalWrite(SD_CS, HIGH); + pinMode(TFT_RST, INPUT); + + SPI.begin(); + + // TFT init + tft.begin(HX8357D); + tft.setRotation(1); // Landscape + + // SD config load + Serial.println("Initializing SD..."); + if (SD.begin(SD_CS, SD_SCK_MHZ(10))) { + File file = SD.open("/sdsetup.json"); + if (file) { + DeserializationError error = deserializeJson(secretdoc, file); + if (!error) { + settings.my_ssid = secretdoc["ssid"].as(); + settings.my_pass = secretdoc["password"].as(); + settings.auth_user = secretdoc["oqm-user"].as(); + settings.auth_pass = secretdoc["oqm-secret"].as(); + settings.remote_host = secretdoc["oqm-address"].as(); + gotSD = true; + Serial.println("SD Config loaded."); + } + file.close(); + } + secretdoc.clear(); + } else { + Serial.println("SD failed — using defaults."); + } + + // WiFi + Serial.printf("Connecting to %s", settings.my_ssid.c_str()); + WiFi.begin(settings.my_ssid, settings.my_pass); + while (WiFi.status() != WL_CONNECTED) { + delay(500); + Serial.print("."); + } + Serial.println("\nWiFi connected!"); + + requestAuth(); + + // Draw initial settings screen — DB not yet chosen, overlay will open + drawSettingsScreen(); + + // Immediately show the DB chooser so the user picks one at startup + populateDBList(); + showingDBList = true; + drawDBListOverlay(); +} + +// ============================================================ +// LOOP +// ============================================================ +void loop() { + + // 1. Token refresh (non-blocking) + if (time_at_auth == 0 || + (millis() - time_at_auth) > ((unsigned long)expiry * 750UL)) { + requestAuth(); + } + + // 2. Touch input + uint16_t x, y, z1, z2; + if (touch.read_touch(&x, &y, &z1, &z2) && z1 > 100) { + int touchX = constrain(map(y, 239, 3780, 0, 480), 0, 479); + int touchY = constrain(map(x, 3640, 367, 0, 320), 0, 319); + + if (millis() - lastTouchTime > 300) { + lastTouchTime = millis(); + + // Overlays take priority + if (showingDBList) { + handleDBListTouch(touchX, touchY); + } else if (showingBlockList) { + handleBlockListTouch(touchX, touchY); + } else { + processTouch(touchX, touchY); + } + } + } + + // 3. UART scanner (non-blocking) + if (qrcode.available()) { + currentBarcode = qrcode.getDecodeData(); + currentBarcode.trim(); + newScanPending = true; + } + + // 4. Process pending scan (only when no API call is in flight + // and no overlay is open) + if (newScanPending && !apiInFlight && + !showingDBList && !showingBlockList) { + newScanPending = false; + HandleNewScan(); + } +} + +// ============================================================ +// SCAN HANDLER +// ============================================================ +void HandleNewScan() { + if (currentScreen == SCREEN_QUICK) { + // Fetch current count, then immediately update ±1 + apiInFlight = true; + GetItem(currentBarcode); + apiInFlight = false; + + if (getdoc["results"][0]["id"].isNull()) { + // Item not found — show briefly on card then return + currentItemName = "Not found"; + currentItemCount = 0; + adjustedCount = 0; + refreshQuickCard(); + return; + } + + item_ID = getdoc["results"][0]["id"].as(); + currentItemName = getdoc["results"][0]["name"].as(); + currentItemCount = getdoc["results"][0]["stats"]["total"]["value"].as(); + adjustedCount = currentItemCount + (quickModeAdd ? 1 : -1); + + refreshQuickCard(); + + apiInFlight = true; + UpdateCount(storage_block, item_ID, quickModeAdd ? 1 : 2, 1); + apiInFlight = false; + + } else if (currentScreen == SCREEN_DETAIL) { + // Fetch item info + apiInFlight = true; + GetItem(currentBarcode); + apiInFlight = false; + + if (getdoc["results"][0]["id"].isNull()) { + currentItemName = "Not found"; + currentItemCount = 0; + adjustedCount = 0; + refreshDetailCard(); + return; + } + + item_ID = getdoc["results"][0]["id"].as(); + currentItemName = getdoc["results"][0]["name"].as(); + currentItemCount = getdoc["results"][0]["stats"]["total"]["value"].as(); + adjustedCount = currentItemCount; + + refreshDetailCard(); + + // Determine storage block for this item + int blocksinItem = getdoc["results"][0]["storageBlocks"].size(); + + if (blocksinItem == 0) { + storage_block = ""; + block_name = "None"; + } else if (blocksinItem == 1) { + // Auto-select the only block + storage_block = getdoc["results"][0]["storageBlocks"][0].as(); + // Look up its label in stblockdoc + block_name = storage_block; // fallback + for (int i = 0; i < (int)stblockdoc["results"].size(); i++) { + if (stblockdoc["results"][i]["id"].as() == storage_block) { + block_name = stblockdoc["results"][i]["label"].as(); + break; + } + } + } else { + // Multiple blocks — show filtered chooser + populateBlockList(true); // true = filter to item's blocks only + showingBlockList = true; + drawBlockListOverlay(); + } + } +} + +// ============================================================ +// TOUCH ROUTING +// ============================================================ +void processTouch(int x, int y) { + switch (currentScreen) { + + // ── Settings screen ───────────────────────────────────── + case SCREEN_SETTINGS: { + // "Change DB" button: x 280-450, y 55-100 + if (x >= 280 && x <= 450 && y >= 55 && y <= 100) { + populateDBList(); + showingDBList = true; + drawDBListOverlay(); + return; + } + // Quick "+" button: x 30-150, y 148-208 + if (x >= 30 && x <= 150 && y >= 148 && y <= 208) { + if (db_name == "") return; + quickModeAdd = true; + GetStorageBlocks(); + populateBlockList(false); + showingBlockList = true; + drawBlockListOverlay(); + previousScreen = SCREEN_QUICK; + return; + } + // Quick "-" button: x 160-280, y 148-208 + if (x >= 160 && x <= 280 && y >= 148 && y <= 208) { + if (db_name == "") return; + quickModeAdd = false; + GetStorageBlocks(); + populateBlockList(false); + showingBlockList = true; + drawBlockListOverlay(); + previousScreen = SCREEN_QUICK; + return; + } + // Detail "Open ->" button: x 155-325, y 248-298 + if (x >= 155 && x <= 325 && y >= 248 && y <= 298) { + if (db_name == "") return; + GetStorageBlocks(); + previousScreen = currentScreen; + currentScreen = SCREEN_DETAIL; + drawDetailScreen(); + return; + } + break; + } + + // ── Quick screen ───────────────────────────────────────── + case SCREEN_QUICK: { + // "+" button: x 20-200, y 197-277 + if (x >= 20 && x <= 200 && y >= 197 && y <= 277) { + quickModeAdd = true; + refreshQuickCard(); + return; + } + // "-" button: x 260-440, y 197-277 + if (x >= 260 && x <= 440 && y >= 197 && y <= 277) { + quickModeAdd = false; + refreshQuickCard(); + return; + } + // "< Back" button: x 30-150, y 268-318 + if (x >= 30 && x <= 150 && y >= 268 && y <= 318) { + previousScreen = currentScreen; + currentScreen = SCREEN_SETTINGS; + drawSettingsScreen(); + return; + } + // "Chg Block" button: x 160-320, y 268-318 + if (x >= 160 && x <= 320 && y >= 268 && y <= 318) { + GetStorageBlocks(); + populateBlockList(false); + showingBlockList = true; + drawBlockListOverlay(); + previousScreen = SCREEN_QUICK; + return; + } + // "Detail >" button: x 330-450, y 268-318 + if (x >= 330 && x <= 450 && y >= 268 && y <= 318) { + previousScreen = currentScreen; + currentScreen = SCREEN_DETAIL; + drawDetailScreen(); + return; + } + break; + } + + // ── Detail screen ──────────────────────────────────────── + case SCREEN_DETAIL: { + // Count "^" up arrow: x 390-445, y 162-207 + if (x >= 390 && x <= 445 && y >= 162 && y <= 207) { + adjustedCount++; + // Redraw just the count area + tft.fillRoundRect(10, 158, 160, 35, 6, COLOR_CARD); + tft.drawRoundRect(10, 158, 160, 35, 6, COLOR_DIVIDER); + tft.setCursor(20, 167); + tft.setTextColor(COLOR_TEXT); + tft.setTextSize(2); + tft.print("Count: "); + tft.setTextColor(COLOR_HEADER); + tft.println(adjustedCount); + return; + } + // Count "v" down arrow: x 390-445, y 215-260 + if (x >= 390 && x <= 445 && y >= 215 && y <= 260) { + if (adjustedCount > 0) adjustedCount--; + tft.fillRoundRect(10, 158, 160, 35, 6, COLOR_CARD); + tft.drawRoundRect(10, 158, 160, 35, 6, COLOR_DIVIDER); + tft.setCursor(20, 167); + tft.setTextColor(COLOR_TEXT); + tft.setTextSize(2); + tft.print("Count: "); + tft.setTextColor(COLOR_HEADER); + tft.println(adjustedCount); + return; + } + // Radio buttons: x 230-460, y 162-302 (5 rows × 28px) + if (x >= 230 && x <= 460 && y >= 162 && y <= 302) { + int newAction = (y - 162) / 28; + if (newAction >= 0 && newAction < 5) { + selectedAction = newAction; + // Redraw all 5 radio buttons + for (int i = 0; i < 5; i++) { + drawRadioButton(230, 159 + i * 26, actionNames[i], selectedAction == i); + } + } + return; + } + // "< Back" button: x 30-150, y 265-315 + if (x >= 30 && x <= 150 && y >= 265 && y <= 315) { + previousScreen = currentScreen; + currentScreen = SCREEN_SETTINGS; + drawSettingsScreen(); + return; + } + // "Submit ->" button: x 290-450, y 265-315 + if (x >= 290 && x <= 450 && y >= 265 && y <= 315) { + if (item_ID == "" || storage_block == "") return; + apiInFlight = true; + UpdateCount(storage_block, item_ID, selectedAction + 1, adjustedCount); + apiInFlight = false; + GetItem(currentBarcode); + currentItemCount = getdoc["results"][0]["stats"]["total"]["value"].as(); + adjustedCount = currentItemCount; + refreshDetailCard(); + return; + } + break; + } + } +} + +// ============================================================ +// OVERLAY TOUCH HANDLERS +// ============================================================ + +// DB list overlay touch +// Overlay bounds: x 40-440, y 55-320 +// Header: y 56-96 (skip) +// Rows: y 97+, 40px each +// Cancel button: x 160-280, y 275-310 +void handleDBListTouch(int x, int y) { + // Cancel + if (x >= 160 && x <= 280 && y >= 275 && y <= 310) { + showingDBList = false; + // Redraw underlying screen to remove overlay + if (currentScreen == SCREEN_SETTINGS) drawSettingsScreen(); + return; + } + // Row tap + if (x >= 41 && x <= 439 && y >= 97 && y < 275) { + int idx = (y - 97) / 40; + if (idx >= 0 && idx < dbCount) { + onDBSelected(idx); + } + } +} + +// Block list overlay touch +void handleBlockListTouch(int x, int y) { + // Cancel + if (x >= 160 && x <= 280 && y >= 275 && y <= 310) { + showingBlockList = false; + if (currentScreen == SCREEN_SETTINGS) drawSettingsScreen(); + else if (currentScreen == SCREEN_QUICK) drawQuickScreen(); + else if (currentScreen == SCREEN_DETAIL) drawDetailScreen(); + return; + } + // Row tap + if (x >= 41 && x <= 439 && y >= 97 && y < 275) { + int idx = (y - 97) / 40; + if (idx >= 0 && idx < blockCount) { + onBlockSelected(idx); + } + } +} + +// ── Called when user selects a DB from the overlay ─────────── +void onDBSelected(int index) { + showingDBList = false; + db_name = dbList[index]; + Serial.printf("DB selected: %s\n", db_name.c_str()); + + // Refresh settings screen with new DB name shown + drawSettingsScreen(); +} + +// ── Called when user selects a block from the overlay ──────── +void onBlockSelected(int index) { + showingBlockList = false; + storage_block = blockListIDs[index]; + block_name = blockList[index]; + Serial.printf("Block selected: %s (%s)\n", + block_name.c_str(), storage_block.c_str()); + + // Navigate if we came from a "+" or "-" button on settings + if (previousScreen == SCREEN_QUICK) { + currentScreen = SCREEN_QUICK; + drawQuickScreen(); + } else if (currentScreen == SCREEN_DETAIL) { + // Block was chosen post-scan in detail mode — just redraw detail + drawDetailScreen(); + } else { + drawSettingsScreen(); + } +} + +// ============================================================ +// LIST POPULATION HELPERS +// ============================================================ + +void populateDBList() { + GetDB(); + dbCount = 0; + for (int i = 0; i < (int)dbdoc.size() && i < 10; i++) { + dbList[i] = dbdoc[i]["name"].as(); + dbCount++; + } +} + +// filteredByItem=true → only blocks that contain getdoc item +// filteredByItem=false → all blocks from stblockdoc +void populateBlockList(bool filteredByItem) { + blockCount = 0; + + if (!filteredByItem) { + for (int i = 0; i < (int)stblockdoc["results"].size() && i < 10; i++) { + blockList[i] = stblockdoc["results"][i]["label"].as(); + blockListIDs[i] = stblockdoc["results"][i]["id"].as(); + blockCount++; + } + return; + } + + // Filtered: cross-reference item's storageBlocks with stblockdoc + int itemBlockCount = getdoc["results"][0]["storageBlocks"].size(); + for (int i = 0; i < (int)stblockdoc["results"].size() && blockCount < 10; i++) { + String bid = stblockdoc["results"][i]["id"].as(); + for (int j = 0; j < itemBlockCount; j++) { + if (getdoc["results"][0]["storageBlocks"][j].as() == bid) { + blockList[blockCount] = stblockdoc["results"][i]["label"].as(); + blockListIDs[blockCount] = bid; + blockCount++; + break; + } + } + } +} + +// ============================================================ +// PARTIAL REDRAWS (avoid full screen flicker) +// ============================================================ + +// Redraws only the item info card and +/- buttons on Quick screen +void refreshQuickCard() { + // Clear card area + tft.fillRoundRect(10, 58, 460, 130, 8, COLOR_CARD); + tft.drawRoundRect(10, 58, 460, 130, 8, COLOR_DIVIDER); + + tft.setCursor(20, 68); + tft.setTextColor(COLOR_HEADER); + tft.setTextSize(2); + tft.println("Last Item:"); + + tft.setCursor(20, 93); + tft.setTextColor(COLOR_TEXT); + tft.print("Barcode: "); + tft.println(currentBarcode); + + tft.setCursor(20, 118); + tft.print("Name: "); + tft.println(currentItemName); + + tft.setCursor(20, 143); + tft.print("Count: "); + tft.setTextColor(COLOR_HEADER); + tft.println(adjustedCount); + + // Redraw +/- with active state highlighted + uint16_t plusColor = quickModeAdd ? COLOR_SELECTED : COLOR_BTN; + uint16_t minusColor = !quickModeAdd ? COLOR_SELECTED : COLOR_BTN_NEG; + drawButton(20, 200, 180, 50, "+", plusColor); + drawButton(260, 200, 180, 50, "-", minusColor); +} + +// Redraws only the item info card on Detail screen +void refreshDetailCard() { + tft.fillRoundRect(10, 58, 460, 95, 8, COLOR_CARD); + tft.drawRoundRect(10, 58, 460, 95, 8, COLOR_DIVIDER); + + tft.setCursor(20, 68); + tft.setTextColor(COLOR_HEADER); + tft.setTextSize(2); + tft.println("Selected Item:"); + + tft.setCursor(20, 93); + tft.setTextColor(COLOR_TEXT); + tft.print("Barcode: "); + tft.println(currentBarcode); + + tft.setCursor(20, 118); + tft.print("Name: "); + tft.println(currentItemName); + + // Refresh count display + tft.fillRoundRect(10, 158, 160, 35, 6, COLOR_CARD); + tft.drawRoundRect(10, 158, 160, 35, 6, COLOR_DIVIDER); + tft.setCursor(20, 167); + tft.setTextColor(COLOR_TEXT); + tft.setTextSize(2); + tft.print("Count: "); + tft.setTextColor(COLOR_HEADER); + tft.println(adjustedCount); +} + +// ============================================================ +// API FUNCTIONS +// ============================================================ + +//Gets JWT key with username and secret for OQM instance, sends to authdoc +void requestAuth() { + if (WiFi.status() != WL_CONNECTED) return; + + WiFiClientSecure secureClient; + secureClient.setInsecure(); + HTTPClient http; + + String url = "https://" + settings.remote_host + ":" + + settings.remote_port + settings.auth_path; + http.begin(secureClient, url); + + String authRaw = settings.auth_user + ":" + settings.auth_pass; + String authHeader = "Basic " + base64::encode(authRaw); + http.addHeader("Content-Type", "application/x-www-form-urlencoded"); + http.addHeader("Authorization", authHeader); + + int code = http.POST("grant_type=client_credentials"); + if (code == HTTP_CODE_OK) { + authdoc.clear(); + deserializeJson(authdoc, http.getStream()); + expiry = authdoc["expires_in"].as(); + time_at_auth = millis(); + Serial.println("Token refreshed."); + } else { + Serial.printf("Auth failed (%d)\n", code); + } + http.end(); +} + +//Gets a list of available databases, sends to dbdoc +void GetDB() { + WiFiClientSecure secureClient; + secureClient.setInsecure(); + HTTPClient http; + + String url = "https://" + settings.remote_host + ":" + + settings.remote_port + settings.db_path; + http.begin(secureClient, url); + http.addHeader("Authorization", + "Bearer " + authdoc["access_token"].as()); + http.addHeader("Accept", "application/json"); + + int code = http.GET(); + if (code == HTTP_CODE_OK) { + dbdoc.clear(); + deserializeJson(dbdoc, http.getStream()); + } else { + Serial.printf("GetDB failed (%d)\n", code); + } + http.end(); +} + +//Gets a list of available storage blocks, sends to stblockdoc +void GetStorageBlocks() { + WiFiClientSecure secureClient; + secureClient.setInsecure(); + HTTPClient http; + + String url = "https://" + settings.remote_host + ":" + + settings.remote_port + + settings.def_path + db_name + settings.stor_path; + http.begin(secureClient, url); + http.addHeader("Authorization", + "Bearer " + authdoc["access_token"].as()); + http.addHeader("Accept", "application/json"); + + int code = http.GET(); + if (code == HTTP_CODE_OK) { + stblockdoc.clear(); + deserializeJson(stblockdoc, http.getStream()); + } else { + Serial.printf("GetStorageBlocks failed (%d)\n", code); + } + http.end(); +} + +//Gets details of item scanned, sends to getdoc +void GetItem(String identifier) { + WiFiClientSecure secureClient; + secureClient.setInsecure(); + HTTPClient http; + + String url = "https://" + settings.remote_host + ":" + + settings.remote_port + + settings.def_path + db_name + + settings.get_path + identifier; + http.begin(secureClient, url); + http.addHeader("Authorization", + "Bearer " + authdoc["access_token"].as()); + http.addHeader("Accept", "application/json"); + + unsigned long start = millis(); + bool success = false; + while (!success && millis() - start < 10000) { + int code = http.GET(); + if (code == HTTP_CODE_OK) { + getdoc.clear(); + DeserializationError err = deserializeJson(getdoc, http.getStream()); + if (!err) success = true; + } else { + Serial.printf("GetItem failed (%d) — retrying\n", code); + delay(2000); + } + } + http.end(); +} + +//Given storage block, item id, transaction type, and value, updates count of an item +void UpdateCount(String stblock, String itemID, + int transaction, int value) { + if (WiFi.status() != WL_CONNECTED) return; + + WiFiClientSecure secureClient; + secureClient.setInsecure(); + HTTPClient http; + + String url = "https://" + settings.remote_host + ":" + + settings.remote_port + + settings.def_path + db_name + + "/inventory/item/" + itemID + "/stored/transaction"; + http.begin(secureClient, url); + http.addHeader("Authorization", + "Bearer " + authdoc["access_token"].as()); + http.addHeader("Content-Type", "application/json"); + http.addHeader("Accept", "application/json"); + + JsonDocument doc; + switch (transaction) { + case 1: { + doc["type"] = "ADD_AMOUNT"; + doc["toBlock"] = stblock; + } + break; + case 2: { + doc["type"] = "SUBTRACT_AMOUNT"; + doc["fromBlock"] = stblock; + } + break; + case 3: { + doc["type"] = "CHECKOUT_AMOUNT"; + doc["fromBlock"] = stblock; + JsonObject checkout = doc["checkoutDetails"].to(); + checkout["reason"] = ""; + checkout["notes"] = ""; + JsonObject checkdet = checkout["checedkOutFor"].to(); + checkdet["entity"] = "6973dbc978a22b6009670776"; + checkdet["type"] = "OQM_ENTITY"; + doc["all"] = false; + } + break; + // case 4: + // // doc["type"] = "CHECKIN_AMOUNT"; + // // doc["toBlock"] = stblock; + // break; + case 5: { + doc["type"] = "SET_AMOUNT"; + doc["block"] = stblock; + } + break; + } + JsonObject amount = doc["amount"].to(); + amount["value"] = value; + amount["scale"] = "ABSOLUTE"; + JsonObject unit = amount["unit"].to(); + unit["string"] = "units"; + + String body; + serializeJson(doc, body); + + int code = http.POST(body); + if (code == HTTP_CODE_OK || code == HTTP_CODE_CREATED) { + Serial.println("Transaction success."); + } else { + Serial.printf("Transaction failed (%d): %s\n", + code, http.getString().c_str()); + } + http.end(); +} diff --git a/hardware/speed-scanner/scannerUI.ino b/hardware/speed-scanner/scannerUI.ino new file mode 100644 index 0000000000..9143a612b8 --- /dev/null +++ b/hardware/speed-scanner/scannerUI.ino @@ -0,0 +1,317 @@ +// ============================================================ +// OQM Scanner UI — scannerUI.ino (Arduino IDE tab 2) +// All drawing functions. No API calls here. +// ============================================================ + +#include "Adafruit_GFX.h" +#include "Adafruit_HX8357.h" +#include "color.h" + +// ── Externs (defined in main .ino) ─────────────────────────── +extern Adafruit_HX8357 tft; + +extern String currentBarcode; +extern String currentItemName; +extern int currentItemCount; +extern int adjustedCount; +extern String db_name; +extern String storage_block; +extern String block_name; + +extern String dbList[]; +extern String blockList[]; +extern String blockListIDs[]; +extern int dbCount; +extern int blockCount; +extern bool showingDBList; +extern bool showingBlockList; + +extern String actionNames[]; +extern int selectedAction; +extern bool quickModeAdd; + +extern Screen currentScreen; +extern Screen previousScreen; + +// ── Forward declarations ────────────────────────────────────── +void drawButton(int x, int y, int w, int h, + String label, uint16_t bgColor); +void drawRadioButton(int x, int y, String label, bool selected); + +// ============================================================ +// SETTINGS SCREEN +// ============================================================ +void drawSettingsScreen() { + tft.fillScreen(COLOR_BG); + + // Header bar + tft.fillRect(0, 0, 480, 50, COLOR_HEADER); + tft.setCursor(10, 12); + tft.setTextColor(COLOR_WHITE); + tft.setTextSize(3); + tft.println("OQM Scanner"); + + // ── DB row ── + tft.setCursor(20, 67); + tft.setTextColor(COLOR_TEXT); + tft.setTextSize(2); + tft.print("DB: "); + // Truncate long names so they don't overwrite the button + String dbDisplay = db_name.length() > 0 ? db_name : "(none)"; + if (dbDisplay.length() > 16) dbDisplay = dbDisplay.substring(0, 15) + "~"; + tft.print(dbDisplay); + // "Change DB" button — inset from right edge, taller + drawButton(280, 55, 170, 45, "Change DB", COLOR_BTN); + + // Divider + tft.drawFastHLine(30, 112, 420, COLOR_DIVIDER); + + // ── Quick mode row ── + tft.setCursor(30, 122); + tft.setTextColor(COLOR_HEADER); + tft.setTextSize(2); + tft.println("Quick Mode (scan -> update block)"); + + // Highlight whichever was last active — inset from edges, taller + uint16_t plusColor = quickModeAdd ? COLOR_SELECTED : COLOR_BTN; + uint16_t minusColor = !quickModeAdd ? COLOR_SELECTED : COLOR_BTN_NEG; + drawButton(30, 148, 120, 60, "+ Add", plusColor); + drawButton(160, 148, 120, 60, "- Sub", minusColor); + + // Divider + tft.drawFastHLine(30, 218, 420, COLOR_DIVIDER); + + // ── Detail mode row ── + tft.setCursor(30, 228); + tft.setTextColor(COLOR_HEADER); + tft.setTextSize(2); + tft.println("Detail Mode (choose action + qty)"); + + // Centred, inset, taller + drawButton(155, 248, 170, 50, "Open ->", COLOR_BTN); + + // Disabled hint when no DB selected + if (db_name.length() == 0) { + tft.setCursor(30, 305); + tft.setTextColor(COLOR_BTN_NEG); + tft.setTextSize(1); + tft.println("Select a database above to begin."); + } +} + +// ============================================================ +// QUICK SCREEN +// ============================================================ +void drawQuickScreen() { + tft.fillScreen(COLOR_BG); + + // Header + tft.fillRect(0, 0, 480, 50, COLOR_HEADER); + tft.setCursor(10, 12); + tft.setTextColor(COLOR_WHITE); + tft.setTextSize(3); + String modeLabel = quickModeAdd ? "Quick Mode +" : "Quick Mode -"; + tft.println(modeLabel); + + // Block label + tft.setCursor(10, 52); + tft.setTextColor(COLOR_TEXT); + tft.setTextSize(1); + tft.print("Block: "); + tft.println(block_name.length() > 0 ? block_name : "(none)"); + + // Item info card + tft.drawRoundRect(10, 62, 460, 125, 8, COLOR_DIVIDER); + tft.setCursor(20, 72); + tft.setTextColor(COLOR_HEADER); + tft.setTextSize(2); + tft.println("Last Item:"); + + tft.setCursor(20, 97); + tft.setTextColor(COLOR_TEXT); + tft.print("Barcode: "); + tft.println(currentBarcode.length() > 0 ? currentBarcode : "—"); + + tft.setCursor(20, 122); + tft.print("Name: "); + tft.println(currentItemName); + + tft.setCursor(20, 147); + tft.print("Count: "); + tft.setTextColor(COLOR_HEADER); + tft.println(adjustedCount); + + // +/- big buttons — selected one is darker + uint16_t plusColor = quickModeAdd ? COLOR_SELECTED : COLOR_BTN; + uint16_t minusColor = !quickModeAdd ? COLOR_SELECTED : COLOR_BTN_NEG; + drawButton(20, 197, 180, 50, "+", plusColor); + drawButton(260, 197, 180, 50, "-", minusColor); + + // Nav row — inset 30px from each edge, 50px tall + drawButton(30, 268, 120, 50, "< Back", COLOR_BTN_NEG); + drawButton(160, 268, 160, 50, "Chg Block", COLOR_BTN_NEG); + drawButton(330, 268, 120, 50, "Detail >", COLOR_BTN); +} + +// ============================================================ +// DETAIL SCREEN +// ============================================================ +void drawDetailScreen() { + tft.fillScreen(COLOR_BG); + + // Header + tft.fillRect(0, 0, 480, 50, COLOR_HEADER); + tft.setCursor(10, 12); + tft.setTextColor(COLOR_WHITE); + tft.setTextSize(3); + tft.println("Detail Mode"); + + // Item info card + tft.drawRoundRect(10, 55, 460, 95, 8, COLOR_DIVIDER); + tft.setCursor(20, 65); + tft.setTextColor(COLOR_HEADER); + tft.setTextSize(2); + tft.println("Selected Item:"); + + tft.setCursor(20, 90); + tft.setTextColor(COLOR_TEXT); + tft.print("Barcode: "); + tft.println(currentBarcode.length() > 0 ? currentBarcode : "— (scan to load)"); + + tft.setCursor(20, 115); + tft.print("Name: "); + tft.println(currentItemName); + + // Block label (auto-set or chosen from overlay) + tft.setCursor(20, 135); + tft.setTextColor(COLOR_DIVIDER); + tft.setTextSize(1); + tft.print("Block: "); + tft.println(block_name.length() > 0 ? block_name : "(scan item first)"); + + // Count box + tft.drawRoundRect(10, 155, 160, 35, 6, COLOR_DIVIDER); + tft.setCursor(20, 164); + tft.setTextColor(COLOR_TEXT); + tft.setTextSize(2); + tft.print("Count: "); + tft.setTextColor(COLOR_HEADER); + tft.println(adjustedCount); + + // Action radio buttons + for (int i = 0; i < 5; i++) { + drawRadioButton(230, 159 + i * 26, actionNames[i], selectedAction == i); + } + + // Up/Down arrows (aligned with count box) + drawButton(390, 155, 55, 45, "^", COLOR_BTN); + drawButton(390, 208, 55, 45, "v", COLOR_BTN); + + // Nav / submit row — inset 30px from each edge, 50px tall + drawButton(30, 265, 120, 50, "< Back", COLOR_BTN_NEG); + drawButton(290, 265, 160, 50, "Submit ->", COLOR_SUBMIT); +} + +// ============================================================ +// DB LIST OVERLAY +// ============================================================ +void drawDBListOverlay() { + // Overlay card + tft.fillRoundRect(40, 50, 400, 270, 8, COLOR_BG); + tft.drawRoundRect(40, 50, 400, 270, 8, COLOR_HEADER); + + // Overlay header + tft.fillRect(41, 51, 398, 40, COLOR_HEADER); + tft.setCursor(55, 61); + tft.setTextColor(COLOR_WHITE); + tft.setTextSize(2); + tft.println("Select Database:"); + + // Rows + for (int i = 0; i < dbCount; i++) { + uint16_t rowColor = (i % 2 == 0) ? COLOR_DIVIDER : COLOR_BG; + tft.fillRect(41, 92 + i * 40, 398, 40, rowColor); + tft.setCursor(55, 100 + i * 40); + tft.setTextColor(COLOR_TEXT); + tft.setTextSize(2); + // Truncate long names + String label = dbList[i]; + if (label.length() > 22) label = label.substring(0, 21) + "~"; + tft.println(label); + } + + // Empty state + if (dbCount == 0) { + tft.setCursor(55, 140); + tft.setTextColor(COLOR_TEXT); + tft.setTextSize(2); + tft.println("No databases found."); + } + + drawButton(160, 272, 120, 35, "Cancel", COLOR_BTN_NEG); +} + +// ============================================================ +// BLOCK LIST OVERLAY +// ============================================================ +void drawBlockListOverlay() { + tft.fillRoundRect(40, 50, 400, 270, 8, COLOR_BG); + tft.drawRoundRect(40, 50, 400, 270, 8, COLOR_HEADER); + + tft.fillRect(41, 51, 398, 40, COLOR_HEADER); + tft.setCursor(55, 61); + tft.setTextColor(COLOR_WHITE); + tft.setTextSize(2); + tft.println("Select Block:"); + + for (int i = 0; i < blockCount; i++) { + uint16_t rowColor = (i % 2 == 0) ? COLOR_DIVIDER : COLOR_BG; + tft.fillRect(41, 92 + i * 40, 398, 40, rowColor); + tft.setCursor(55, 100 + i * 40); + tft.setTextColor(COLOR_TEXT); + tft.setTextSize(2); + String label = blockList[i]; + if (label.length() > 22) label = label.substring(0, 21) + "~"; + tft.println(label); + } + + if (blockCount == 0) { + tft.setCursor(55, 140); + tft.setTextColor(COLOR_TEXT); + tft.setTextSize(2); + tft.println("No blocks found."); + } + + drawButton(160, 272, 120, 35, "Cancel", COLOR_BTN_NEG); +} + +// ============================================================ +// SHARED DRAWING HELPERS +// ============================================================ + +void drawButton(int x, int y, int w, int h, + String label, uint16_t color) { + tft.fillRoundRect(x, y, w, h, 6, color); + + // Text color: dark on light buttons, white on colored ones + uint16_t textColor = (color == COLOR_BTN_NEG) ? COLOR_TEXT : COLOR_WHITE; + tft.setTextColor(textColor); + tft.setTextSize(2); + + // Centre the label + int charW = 12; // ~12px per char at size 2 + int textX = x + (w - (int)label.length() * charW) / 2; + int textY = y + (h / 2) - 8; + tft.setCursor(textX, textY); + tft.print(label); +} + +void drawRadioButton(int x, int y, String label, bool selected) { + uint16_t fillColor = selected ? COLOR_SELECTED : COLOR_BG; + tft.fillCircle(x + 8, y + 8, 8, fillColor); + tft.drawCircle(x + 8, y + 8, 8, COLOR_HEADER); + tft.setCursor(x + 22, y); + tft.setTextColor(COLOR_TEXT); + tft.setTextSize(2); + tft.println(label); +} diff --git a/hardware/speed-scanner/sdcard/sdsetup.json b/hardware/speed-scanner/sdcard/sdsetup.json new file mode 100644 index 0000000000..351a40f996 --- /dev/null +++ b/hardware/speed-scanner/sdcard/sdsetup.json @@ -0,0 +1,7 @@ +{ + "ssid": "REPLACE_ME", + "password": "REPLACE_ME", + "oqm-address": "REPLACE_ME", + "oqm-user": "REPLACE_ME", + "oqm-secret": "REPLACE_ME" +} diff --git a/software/core/oqm-core-api/README.md b/software/core/oqm-core-api/README.md index 011e30fff4..513e3f7daa 100644 --- a/software/core/oqm-core-api/README.md +++ b/software/core/oqm-core-api/README.md @@ -1,5 +1,6 @@ -# open-qm-base-station +# Open QuarterMaster Core API +![Core API](https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/actions/workflows/core-api.yml/badge.svg) ![Docker Image Version](https://img.shields.io/docker/v/ebprod/oqm-core-api?label=Docker%20Image) diff --git a/software/core/oqm-core-api/gradle/wrapper/gradle-wrapper.properties b/software/core/oqm-core-api/gradle/wrapper/gradle-wrapper.properties index b52fb7e713..df6a6ad763 100644 --- a/software/core/oqm-core-api/gradle/wrapper/gradle-wrapper.properties +++ b/software/core/oqm-core-api/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/software/core/oqm-core-base-station/README.md b/software/core/oqm-core-base-station/README.md index 214e64d538..097a1310a4 100644 --- a/software/core/oqm-core-base-station/README.md +++ b/software/core/oqm-core-base-station/README.md @@ -1,12 +1,14 @@ -# oqm-core-base-station +# OQM Base Station -TODOS for database: +![Base Station](https://github.com/Epic-Breakfast-Productions/OpenQuarterMaster/actions/workflows/core-baseStation.yml/badge.svg) +![Docker Image Version](https://img.shields.io/docker/v/ebprod/oqm-core-base_station?label=Docker%20Image) - - handle oqm db selected not present - - make utility to handle case of no databases available to user +This service is the default user interface for the Open QuarterMaster system. +It is intended to be a near 1:1 match for functionality to the API, and provide an interface through which to manage your inventory. +## Quarkus stuff This project uses Quarkus, the Supersonic Subatomic Java Framework. diff --git a/software/core/oqm-core-base-station/gradle/wrapper/gradle-wrapper.properties b/software/core/oqm-core-base-station/gradle/wrapper/gradle-wrapper.properties index b52fb7e713..df6a6ad763 100644 --- a/software/core/oqm-core-base-station/gradle/wrapper/gradle-wrapper.properties +++ b/software/core/oqm-core-base-station/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/software/core/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/interfaces/RestInterface.java b/software/core/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/interfaces/RestInterface.java index 09187d21e3..11aafd3b7c 100644 --- a/software/core/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/interfaces/RestInterface.java +++ b/software/core/oqm-core-base-station/src/main/java/tech/ebp/oqm/core/baseStation/interfaces/RestInterface.java @@ -156,8 +156,37 @@ protected Uni addParentLabelsToSearchResults(ObjectNode results, Str }); } + boolean isDbExist(String dbId){ + if (dbId == null || dbId.isBlank()) { + return false; + } + try { + ArrayNode fresh = this.oqmDatabases; + if (fresh != null && this.containsDb(fresh, dbId)) { + return true; + } + } catch (Exception e) { + log.warn("Failed to verify DB {}", dbId, e); + return false; + } + log.warn("DB {} does not exist", dbId); + return false; + } + + private boolean containsDb(ArrayNode dbs, String dbId) { + for (JsonNode db : dbs) { + JsonNode idNode = db.get("id"); + JsonNode nameNode = db.get("name"); + if ((idNode != null && dbId.equals(idNode.asText())) + || (nameNode != null && dbId.equals(nameNode.asText()))) { + return true; + } + } + return false; + } + public String getSelectedDb() { - if (this.oqmDb == null || this.oqmDb.isBlank()) { + if (!this.isDbExist(this.oqmDb)) { if(this.oqmDatabases == null || this.oqmDatabases.isEmpty()){ throw new IllegalStateException("Cannot have no databases."); } diff --git a/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/interfaces/ui/pages/ItemsUiTest.java b/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/interfaces/ui/pages/ItemsUiTest.java index 8768ded3ba..0124972d4f 100644 --- a/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/interfaces/ui/pages/ItemsUiTest.java +++ b/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/interfaces/ui/pages/ItemsUiTest.java @@ -71,8 +71,8 @@ public void testAddItemNoBlock(String itemType) { oqm.locator(ItemsPage.VIEW_NAME).textContent() ); assertEquals( - expectedDescription, - oqm.locator(ItemsPage.VIEW_DESCRIPTION).textContent() + expectedDescription.strip(), + oqm.locator(ItemsPage.VIEW_DESCRIPTION).textContent().strip() ); Locator viewModal = oqm.locator(ItemsPage.VIEW_MODAL); @@ -143,8 +143,8 @@ public void testAddItemWithBlock(String itemType) { oqm.locator(ItemsPage.VIEW_NAME).textContent() ); assertEquals( - expectedDescription, - oqm.locator(ItemsPage.VIEW_DESCRIPTION).textContent() + expectedDescription.strip(), + oqm.locator(ItemsPage.VIEW_DESCRIPTION).textContent().strip() ); Locator viewModal = oqm.locator(ItemsPage.VIEW_MODAL); diff --git a/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/testResources/testClasses/RunningServerTest.java b/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/testResources/testClasses/RunningServerTest.java index a42ec46d1d..83f79d07ad 100644 --- a/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/testResources/testClasses/RunningServerTest.java +++ b/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/testResources/testClasses/RunningServerTest.java @@ -6,6 +6,7 @@ import io.restassured.http.Header; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -29,9 +30,9 @@ public abstract class RunningServerTest extends WebServerTest { @TestHTTPResource("/") URL index; - @Getter - @ConfigProperty(name = "oqm.core.api.baseUri") - String coreApiBaseUri; + protected String getCoreApiBaseUri() { + return ConfigProvider.getConfig().getValue("oqm.core.api.baseUri", String.class); + } @Getter private final TestUserService testUserService = TestUserService.getInstance(); diff --git a/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/testResources/testClasses/WebUiTest.java b/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/testResources/testClasses/WebUiTest.java index 566239f884..1acf7893c9 100644 --- a/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/testResources/testClasses/WebUiTest.java +++ b/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/testResources/testClasses/WebUiTest.java @@ -137,7 +137,7 @@ protected Page getPage() { } try { message.append(curHandle.jsonValue().toString().strip()).append("\n"); - } catch(PlaywrightException e){ + } catch(PlaywrightException | NullPointerException e){ log.warn("Failed to get json value for handle: {}", curHandle, e); break; } diff --git a/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/testResources/ui/pages/ItemsPage.java b/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/testResources/ui/pages/ItemsPage.java index be7cf8d2e7..6ee4265224 100644 --- a/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/testResources/ui/pages/ItemsPage.java +++ b/software/core/oqm-core-base-station/src/test/java/tech/ebp/oqm/core/baseStation/testResources/ui/pages/ItemsPage.java @@ -15,7 +15,7 @@ public class ItemsPage { public static final String ADD_EDIT_FORM = "#addEditItemForm"; public static final String ADD_EDIT_FORM_SUBMIT_BUTTON = "#addEditItemFormSubmitButton"; public static final String ADDEDIT_FORM_INPUT_NAME = "#addEditItemNameInput"; - public static final String ADDEDIT_FORM_INPUT_DESCRIPTION = "#addEditItemDescriptionInput"; + public static final String ADDEDIT_FORM_INPUT_DESCRIPTION = "#addEditItemDescriptionInput .overtype-wrapper textarea.overtype-input"; public static final String ADDEDIT_FORM_INPUT_TYPE = "#addEditItemStorageTypeInput"; public static final String ADDEDIT_FORM_STORAGE_CONTAINER = "#addEditItemAssociatedStorageInputContainer"; public static final String ADDEDIT_FORM_ADD_STORAGE_BUTTON = "#addEditItemAssociatedStorageAddButton"; diff --git a/software/plugins/external-item-search/build.gradle b/software/plugins/external-item-search/build.gradle index 9dae9adbbf..d2cbb0f2b5 100644 --- a/software/plugins/external-item-search/build.gradle +++ b/software/plugins/external-item-search/build.gradle @@ -28,7 +28,7 @@ dependencies { implementation 'io.quarkus:quarkus-container-image-docker' implementation 'io.quarkus:quarkus-cache' implementation 'io.quarkus:quarkus-arc' - implementation 'io.quarkiverse.wiremock:quarkus-wiremock:1.6.1' + implementation 'io.quarkiverse.wiremock:quarkus-wiremock:1.6.3' implementation group: 'org.jsoup', name: 'jsoup', version: '1.22.2' testImplementation 'io.quarkus:quarkus-junit5' diff --git a/software/plugins/external-item-search/gradle/wrapper/gradle-wrapper.properties b/software/plugins/external-item-search/gradle/wrapper/gradle-wrapper.properties index 743f487017..4dcb8425b3 100644 --- a/software/plugins/external-item-search/gradle/wrapper/gradle-wrapper.properties +++ b/software/plugins/external-item-search/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/software/plugins/storagotchi/build.gradle b/software/plugins/storagotchi/build.gradle index 51b461aba7..481a56bcb4 100644 --- a/software/plugins/storagotchi/build.gradle +++ b/software/plugins/storagotchi/build.gradle @@ -30,7 +30,7 @@ dependencies { implementation 'org.webjars:jquery:4.0.0' - implementation 'tech.epic-breakfast-productions.openquartermaster.lib.core:core-api-lib-quarkus:4.4.7' + implementation 'tech.epic-breakfast-productions.openquartermaster.lib.core:core-api-lib-quarkus:4.4.8' testImplementation 'io.quarkus:quarkus-junit5' testImplementation 'io.rest-assured:rest-assured' diff --git a/software/plugins/storagotchi/gradle/wrapper/gradle-wrapper.properties b/software/plugins/storagotchi/gradle/wrapper/gradle-wrapper.properties index 01fa127ce2..dcbba6dd66 100644 --- a/software/plugins/storagotchi/gradle/wrapper/gradle-wrapper.properties +++ b/software/plugins/storagotchi/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=a3c4ba4aca8f0075688b9c5b18939fd28e8cb4357c227da5c1d9f38343791439 -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-all.zip +distributionSha256Sum=c72fb9991f6025cbe337d52ba77e531b3faf62bdd3e348fe1ccee9f51c71adb0 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-all.zip networkTimeout=10000 retries=0 retryBackOffMs=500