diff --git a/CHANGELOG.md b/CHANGELOG.md index 05da4d1..240a206 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ What changed in each version of OpenTag Station, newest first. +## 1.0.1-beta.6 + +Pre-release for testers. One new setting. Not run on a WT32 by the author; see "Needs a station". + +**Display** +- **The screen can be turned upside down.** Browser: **Settings → Display → Screen orientation**, + **Normal** or **Upside down (turned 180°)**. The touchscreen, including touch, turns at once and + keeps the orientation after a restart. Settings key `device.display_rotation`, `0` or `180`; + `GET /api/v1/config` reports it and `PATCH /api/v1/config` accepts it under `device`. Any other + value is refused. Older saved settings without the key mean `0`. + +**Needs a station** +- Set the orientation to upside down in the browser and check that the screen turns without a + restart, that touches land where they are shown (tap each of the five pages), that the setting + survives a reboot, and that **Normal** turns it back. + ## 1.0.1-beta.5 Pre-release for testers. First step of the tag-support roadmap (#93): the reader now also looks diff --git a/README.md b/README.md index 7e1c91b..ea94bdc 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,9 @@ The station writes to two things you care about, your tags and your inventory, s | NFC reader | ELECHOUSE NFC_ST25R3916B module, switched to I²C mode | | Tags | Blank NXP ICODE SLIX2 stickers (ISO 15693 / NFC-V), for OpenPrintTag. NTAG and MIFARE tags are not detected yet (see the [roadmap](#roadmap)). See [Which tags it works with](#which-tags-it-works-with). | | Power | Regulated 5 V over USB | +| Enclosure | A three-part printed case, no supports, about 299 g of filament: [OpenTag-Station on Printables](https://www.printables.com/model/1885872-opentag-station) (CC BY-NC-SA 4.0). Assembly is in the [manual](https://76cb.github.io/OpenTag-Station-Docs/hardware/mechanical/). | -There is exactly one supported controller and one supported reader, with fixed pins, so buy the parts named in the [parts list](https://76cb.github.io/OpenTag-Station-Docs/hardware/bill-of-materials/) rather than look-alikes. There is no reference enclosure; you design the scale platform yourself. +There is exactly one supported controller and one supported reader, with fixed pins, so buy the parts named in the [parts list](https://76cb.github.io/OpenTag-Station-Docs/hardware/bill-of-materials/) rather than look-alikes. The STL files live on Printables, not in this repository.

Wiring diagram: the WT32-SC01 Plus expansion connector to the NAU7802 scale board and the ST25R3916B NFC module @@ -138,7 +139,7 @@ On any other Spoolman or FilaBridge version the station stays connected and read ## Get started -1. **Build it.** Follow the [parts list](https://76cb.github.io/OpenTag-Station-Docs/hardware/bill-of-materials/) and the [wiring guide](https://76cb.github.io/OpenTag-Station-Docs/hardware/wiring/). +1. **Build it.** Follow the [parts list](https://76cb.github.io/OpenTag-Station-Docs/hardware/bill-of-materials/) and the [wiring guide](https://76cb.github.io/OpenTag-Station-Docs/hardware/wiring/), and print the [case](https://www.printables.com/model/1885872-opentag-station). 2. **Install the firmware.** Connect the board with a USB data cable and open the [USB installer](https://76cb.github.io/OpenTag-Station/) ([guide](https://76cb.github.io/OpenTag-Station-Docs/installation/web-flasher/)). A USB install erases all settings and the scale calibration. 3. **Join your Wi-Fi.** The touchscreen shows an open setup network named `OpenTag-Setup-XXXX`. Enter your Wi-Fi on the touchscreen, or join that network and open `http://192.168.4.1/` ([guide](https://76cb.github.io/OpenTag-Station-Docs/getting-started/first-boot/)). Afterwards the station is at `http://opentag-station.local/`. 4. **Prepare Spoolman.** On Spoolman 0.27 or later there is nothing to do. On older versions, add two extra fields of type **Text** to the **Spool** entity, with the keys `opentag_instance_uuid` and `nfc_uid` ([how](https://76cb.github.io/OpenTag-Station-Docs/inventory/custom-fields/)). Without them the station cannot link a tag to a spool. diff --git a/VERSION b/VERSION index 11d6def..6ba6f8e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.1-beta.5 +1.0.1-beta.6 diff --git a/docs/images/assignment.png b/docs/images/assignment.png index e8528c4..6602c99 100644 Binary files a/docs/images/assignment.png and b/docs/images/assignment.png differ diff --git a/docs/images/dashboard.png b/docs/images/dashboard.png index d433f2a..4096d9c 100644 Binary files a/docs/images/dashboard.png and b/docs/images/dashboard.png differ diff --git a/docs/images/provenance.json b/docs/images/provenance.json index cc75402..64581db 100644 --- a/docs/images/provenance.json +++ b/docs/images/provenance.json @@ -9,7 +9,7 @@ "tools/capture_product_review.cjs": "23d6c11b616ac2c271e980410b162d71cf69b72575a452926794bf4838fe22d0", "tools/render_touch_review.py": "8cb523c706690f681f2dc887a726b21e99de020a4d8ec00a9708982073daeeac", "tools/check_public_privacy.py": "b5b3fc8482b8e9692e1524002b911cbcace8c213fd180454469f7c4963ebacd9", - "src/web/web_assets.cpp": "b565808d7de2cb95615d7ae49db614e4303aec28e91f5ae3ab8c61fa3eefb84c", + "src/web/web_assets.cpp": "61fdc3cd86d95272ab217b29c5f81fde6290625a97c33a5986a96bbb14bc962d", "src/web/writer_assets.cpp": "18ca31fb929c43f13138fbd4839e4ed60dc10e342cc2c0dbabb6e95d4c8a48c7", "src/web/writer_layout.inc": "c8835b92d1951269254fec884fe210fc725060528e5505e18a207fbe23d67f98", "src/ui/product_layout.hpp": "4b3139a7f8e95142f53b72a4c9f4ac05d103426a29210885823b30b3171c8979", @@ -18,10 +18,10 @@ "test/test_touch_flow/fixtures.hpp": "e895bf1032a0ddb04a71178fdc8ac3169fb3457274c376ff3e5bbf333f4efe1a" }, "images": { - "docs/images/dashboard.png": "5a532d455b3d67bd8199043b2b11bb1deba036ae776484082fcac62fd14aace5", - "docs/images/weigh.png": "2a2b2b70419ec4038aa37ef2ad630ef77f13cf2230f444a684805f1750df6bc4", - "docs/images/assignment.png": "60dcdb03a77d12639f0964a8fc2d26d441a9e4c4aa7fb51ae7f7af53387a1362", - "docs/images/tag-management.png": "41772e6e29be1725052d1df0e352fa874850da612c915ed112d030536057df1c", + "docs/images/dashboard.png": "09157492a3d145f557dac3cec7c1924fa30b41f875b3e5943d1c04d455c9fa26", + "docs/images/weigh.png": "9c961a746f6d6063e5a9c894c2736bbfdb5ae6caa0cacef28267bf55b2d3f47b", + "docs/images/assignment.png": "6d8683ed09f8829383c39a52a470df7741235694fb81132e0d1cfd8c9149543c", + "docs/images/tag-management.png": "d32cc3e87a3c3191c63a2cc2a6f834559380e63ed53186e87376eaa2305bcfae", "docs/images/wt32-home.png": "c768e1b3be568f2563bdf0a368f2cf5b4aab2e2e6efb8a98c8a996ff4dcdc7e3" } } diff --git a/docs/images/tag-management.png b/docs/images/tag-management.png index 75c0f8c..fb37298 100644 Binary files a/docs/images/tag-management.png and b/docs/images/tag-management.png differ diff --git a/docs/images/weigh.png b/docs/images/weigh.png index 4585cca..ab4ef05 100644 Binary files a/docs/images/weigh.png and b/docs/images/weigh.png differ diff --git a/documentation-site/docs/assets/images/browser/assignment.png b/documentation-site/docs/assets/images/browser/assignment.png index e8528c4..6602c99 100644 Binary files a/documentation-site/docs/assets/images/browser/assignment.png and b/documentation-site/docs/assets/images/browser/assignment.png differ diff --git a/documentation-site/docs/assets/images/browser/dashboard.png b/documentation-site/docs/assets/images/browser/dashboard.png index d433f2a..4096d9c 100644 Binary files a/documentation-site/docs/assets/images/browser/dashboard.png and b/documentation-site/docs/assets/images/browser/dashboard.png differ diff --git a/documentation-site/docs/assets/images/browser/tag-management.png b/documentation-site/docs/assets/images/browser/tag-management.png index 75c0f8c..fb37298 100644 Binary files a/documentation-site/docs/assets/images/browser/tag-management.png and b/documentation-site/docs/assets/images/browser/tag-management.png differ diff --git a/documentation-site/docs/assets/images/browser/weigh.png b/documentation-site/docs/assets/images/browser/weigh.png index 4585cca..ab4ef05 100644 Binary files a/documentation-site/docs/assets/images/browser/weigh.png and b/documentation-site/docs/assets/images/browser/weigh.png differ diff --git a/documentation-site/docs/configuration/display.md b/documentation-site/docs/configuration/display.md index 801f5e7..4bd3eb7 100644 --- a/documentation-site/docs/configuration/display.md +++ b/documentation-site/docs/configuration/display.md @@ -1,7 +1,7 @@ # Display -The only display setting you can change is the brightness. The screen dims and switches off by -itself when nobody touches it. +You can change the brightness and turn the screen upside down. The screen dims and switches off +by itself when nobody touches it. ## Change the brightness @@ -24,6 +24,19 @@ finger. The touchscreen changes to the new brightness straight away; you do not need to restart. +## Turn the screen upside down + +Use this when the board sits the other way round in your case. The whole touchscreen, including +touch, turns by 180°; nothing else changes. + +1. In the browser, go to **Settings** → **Display**. +2. Set **Screen orientation** to **Upside down (turned 180°)**. **Normal** is the default. +3. Click **Validate and save**. + +The touchscreen turns straight away and keeps the orientation after a restart. There is no control +for this on the touchscreen itself. The setting key is `device.display_rotation`, `0` or `180`; see +[Configuration keys](../reference/configuration.md). + ## Dimming and sleep - After 2 minutes without a touch the screen dims to 20 %. diff --git a/documentation-site/docs/contributing/release.md b/documentation-site/docs/contributing/release.md index b7c78eb..2919d3f 100644 --- a/documentation-site/docs/contributing/release.md +++ b/documentation-site/docs/contributing/release.md @@ -5,7 +5,7 @@ publishes it, then updates the USB installer from the same files. Before a release, run the [hardware acceptance](testing.md#hardware-acceptance) checks on a real station. -The current version is `1.0.1-beta.5`. +The current version is `1.0.1-beta.6`. ## Version names diff --git a/documentation-site/docs/getting-started/overview.md b/documentation-site/docs/getting-started/overview.md index 59a45ec..160bfb8 100644 --- a/documentation-site/docs/getting-started/overview.md +++ b/documentation-site/docs/getting-started/overview.md @@ -30,7 +30,7 @@ keep its own inventory. | You need | Notes | |---|---| -| The station hardware | A WT32-SC01 Plus touchscreen board, an NFC reader, a load cell with its amplifier, and a platform you build. See the [parts list](../hardware/bill-of-materials.md). | +| The station hardware | A WT32-SC01 Plus touchscreen board, an NFC reader, a load cell with its amplifier, and the [printed case](../hardware/mechanical.md) or a platform you build. See the [parts list](../hardware/bill-of-materials.md). | | Spoolman | Your own running Spoolman server that the station can reach on your network. The station is not useful without it. Weights are saved to Spoolman 0.26 and 0.27; see [Connect Spoolman](../inventory/spoolman.md). | | 2.4 GHz Wi-Fi | The station cannot join 5 GHz-only or WPA3-only networks. | | Blank tags | NXP ICODE SLIX2 (NFC-V / ISO 15693). NTAG and MIFARE tags are found but not read yet; other tag types are not seen at all. See [Which tags work](../openprinttag/supported-tags.md). | @@ -44,7 +44,7 @@ Follow these pages in order: 1. [Parts list](../hardware/bill-of-materials.md) — buy the parts. 2. [Wiring](../hardware/wiring.md) — connect the scale and the NFC reader, then check - [Power](../hardware/power.md) and build the [scale platform](../hardware/mechanical.md). + [Power](../hardware/power.md) and print the [case](../hardware/mechanical.md) or build your own platform. 3. [Install the firmware over USB](../installation/web-flasher.md). 4. [First boot and Wi-Fi](first-boot.md) — get the station onto your network. 5. [Connect Spoolman and FilaBridge](initial-setup.md) — including the diff --git a/documentation-site/docs/hardware/bill-of-materials.md b/documentation-site/docs/hardware/bill-of-materials.md index 2aa65e7..1cb8d2e 100644 --- a/documentation-site/docs/hardware/bill-of-materials.md +++ b/documentation-site/docs/hardware/bill-of-materials.md @@ -12,13 +12,13 @@ so buy these parts and not look-alikes. | Scale converter | 1 | **NAU7802** breakout board | See [Choosing a NAU7802 board](#choosing-a-nau7802-board) below. | | Load cell | 1 | **YZC-133**, **5 kg** (default) or **2 kg** | A four-wire bar-type cell. Get the seller's wiring sheet and mounting drawing. Use one cell, not two. | | NFC reader | 1 | **ELECHOUSE NFC_ST25R3916B** module (ST25R3916B chip, antenna on the board) | The module must be switched to I2C mode with its solder bridge (see [Wiring](wiring.md)). The pin order in this manual is for this module only. | -| NFC tags | one per spool | **NXP ICODE SLIX2** (NFC-V / ISO 15693), blank | NTAG and MIFARE tags are not seen by the reader at all. Other ISO 15693 tags are seen but cannot be written. See [Which tags work](../openprinttag/supported-tags.md). | +| NFC tags | one per spool | **NXP ICODE SLIX2** (NFC-V / ISO 15693), blank | NTAG and MIFARE tags are found but not read. Other ISO 15693 tags are read but cannot be written. See [Which tags work](../openprinttag/supported-tags.md). | | USB cable | 1 | A data cable that fits the board's USB socket | Charge-only cables cannot install the firmware. | | 5 V power supply | 1 | A regulated 5 V USB supply | See [Power](power.md). | | Cable for the expansion connector | 1 | An 8-wire cable that fits the board's EXT connector | Match the connector on your board. | | Cable for the NFC module | 1 | A 7-wire cable that fits the module's connector | Match the connector on your module. | | Hook-up wire, pin headers | as needed | For the NAU7802 board and the load cell | Depends on your breakout board. | -| Base, platform, screws, spacers | 1 set | To mount the load cell | You design this yourself. See [Scale platform and enclosure](mechanical.md). | +| Enclosure | 1 | The [printed case](mechanical.md#the-printed-case) (three parts, about 299 g of filament), or a base and platform you build | The case needs 2 × M5 nuts, 2 × M5 × 12 button-head screws, 2 × M5 × 16 cap screws, 2 × M2.5 × 10 screws with nuts, four 12 mm rubber feet, a USB-C panel-mount cable and a right-angle USB-C plug. See [Scale platform and enclosure](mechanical.md). | ### Choosing a NAU7802 board @@ -36,7 +36,6 @@ The project has not tested or measured any of these, so it names none: - **Sellers and prices.** - **A specific NAU7802 board.** Any breakout that meets the checks above should work. -- **An enclosure.** There is no reference case, no printable model and no drawing with dimensions. - **A current rating for the power supply.** ## For the curious: how the parts fit the firmware diff --git a/documentation-site/docs/hardware/mechanical.md b/documentation-site/docs/hardware/mechanical.md index e96d214..270eca5 100644 --- a/documentation-site/docs/hardware/mechanical.md +++ b/documentation-site/docs/hardware/mechanical.md @@ -1,10 +1,74 @@ # Scale platform and enclosure -The project has no reference enclosure: no printable case, no drawing, no hole pattern. -You build the base and the platform yourself. -This page lists what matters for weighing accurately and reading tags reliably. +There is a printed case for the station, designed by the project's author: +[OpenTag-Station on Printables](https://www.printables.com/model/1885872-opentag-station) +(Creative Commons BY-NC-SA 4.0). You can also build your own base and platform; the second half of +this page lists what matters then for weighing accurately and reading tags reliably. -## Mount the load cell +## The printed case + +Three parts, about 299 g of filament, no supports. The reader tower at the back holds the NFC +module and is also the backstop for the spool. The body is closed, with the screen flush in the +top; everything goes in from above, and there is nothing to open underneath. The pockets fit a +right-angle USB-C plug and the WT32's side-entry JST plugs. The ring on the tower is 54 mm across +and plain, for a logo or a sticker. + +The STL files are not in this repository; download them from the Printables page. + +| File | Orientation | Notes | +|---|---|---| +| `scale_body.stl` | Upright, flat floor on the bed | Footprint 154 × 190 mm, 120 mm tall: the bed must be at least 195 mm in Y. No real overhangs; the tunnel roofs are flat bridges of 14 mm or less. | +| `scale_platform.stl` | Flat side down | The channel underneath is a 14.6 mm bridge. | +| `nfc_cap.stl` | On its back, as exported | | + +Print settings: PLA or PETG, 0.2 mm layers, 3 to 4 walls, 15 % gyroid infill, no supports. The +files are exported in print orientation; import them as they are. + +Hardware for the case, in addition to the [parts list](bill-of-materials.md): + +- 2 × M5 nuts and 2 × M5 × 12 button-head screws (ISO 7380, head height 2.8 mm or less) +- 2 × M5 × 16 cap screws +- 2 × M2.5 × 10 screws with nuts +- 4 × 12 mm stick-on rubber feet (or print your own in TPU) +- A USB-C panel-mount cable and a right-angle USB-C plug + +### Assembly + +1. **Load cell.** Slide two M5 nuts into the channel at the end of the load-cell pedestal (left + end, reachable through the platform bay). Set the cell on the pedestal with the wires toward + the left, then screw the button-head screws down into the nuts. +2. **USB-C panel cable.** Put the flange in the rear recess and pass the plug end through into + the bay. Secure it with the two M2.5 screws and nuts; the hex traps open into the bay. Route + it through the right tunnel and the load-cell wires through the left tunnel. Both come out in + the cavity under the screen. +3. **NAU7802.** Place it between the locator lugs on the cavity floor, under the screen. +4. **NFC reader.** Drop it into the tower slot from the top. Its wires go down the shaft inside + the tower and come out into the platform bay. Run them across the bay into the left or right + tunnel with the other cables, then press the cap on. +5. **Platform.** Put it on the cell's free end with the two M5 × 16 screws through the oblong + pocket. It sits straight on the cell, with no spacer. +6. **Screen.** Orientation matters, because the cavity has pockets for the plugs. Seen from the + front, the WT32's USB-C end goes on the left and the edge with the I/O, DEBUG and SPK JST + connectors at the top, toward the back. The right-angle USB-C plug fits untrimmed in the + front-left corner; the JST plugs and their wire bend have a pocket under the back edge. Plug in + the cables, then press the WT32 straight down until the glass is flush with the top. Four + 0.2 mm friction ribs on the side walls hold it, and it rests on a 3 mm ledge all around. +7. **Feet.** Stick the four rubber feet under the body. + +To remove the screen, use a small suction cup of the phone-repair kind. If the screen is loose on +your print, two thin strips of double-sided foam tape on the ledge fix it. If the picture comes up +upside down for the way the board sits, turn it in the browser: **Settings → Display → Screen +orientation**, see [Display](../configuration/display.md#turn-the-screen-upside-down). + +Put the spool on with its tagged flange facing the back tower, and slide it in from the front +until it touches. + +## Build your own base and platform + +If you do not print the case, the rules below apply. They also explain why the case is shaped +the way it is. + +### Mount the load cell The YZC-133 is a bar that bends very slightly under load. One end is fixed to the base, the other end carries the platform. @@ -21,7 +85,7 @@ Use the mounting drawing that came with your cell for hole positions and screw s A tight cable acts as a spring and shifts the zero point. 6. Make the platform large and level enough that a spool cannot tip or hang over an edge. -## Place the NFC reader +### Place the NFC reader The antenna is on the NFC module itself. diff --git a/documentation-site/docs/reference/configuration.md b/documentation-site/docs/reference/configuration.md index 55017f2..eca0eda 100644 --- a/documentation-site/docs/reference/configuration.md +++ b/documentation-site/docs/reference/configuration.md @@ -29,6 +29,7 @@ calibration. |---|---|---| | `hostname` | `opentag-station` | 1–63 characters of `a–z 0–9 -`, not starting or ending with `-` | | `brightness_percent` | `80` | 5–100 | +| `display_rotation` | `0` | `0` or `180`. Turns the touchscreen, including touch, upside down. Applied at once, no restart. | | `dim_after_ms` | `120000` | 1–86,400,000 | | `sleep_after_ms` | `300000` | `dim_after_ms`–86,400,000 | | `update_channel` | `stable` | `stable`, `beta` or `development`. Stored only; it changes nothing. | diff --git a/documentation-site/docs/reference/release-notes.md b/documentation-site/docs/reference/release-notes.md index 576e018..72c58d2 100644 --- a/documentation-site/docs/reference/release-notes.md +++ b/documentation-site/docs/reference/release-notes.md @@ -2,6 +2,22 @@ What changed in each version of OpenTag Station, newest first. +## 1.0.1-beta.6 + +Pre-release for testers. One new setting. Not run on a WT32 by the author; see "Needs a station". + +**Display** +- **The screen can be turned upside down.** Browser: **Settings → Display → Screen orientation**, + **Normal** or **Upside down (turned 180°)**. The touchscreen, including touch, turns at once and + keeps the orientation after a restart. Settings key `device.display_rotation`, `0` or `180`; + `GET /api/v1/config` reports it and `PATCH /api/v1/config` accepts it under `device`. Any other + value is refused. Older saved settings without the key mean `0`. + +**Needs a station** +- Set the orientation to upside down in the browser and check that the screen turns without a + restart, that touches land where they are shown (tap each of the five pages), that the setting + survives a reboot, and that **Normal** turns it back. + ## 1.0.1-beta.5 Pre-release for testers. First step of the tag-support roadmap (#93): the reader now also looks diff --git a/documentation-site/firmware.json b/documentation-site/firmware.json index be2e930..cfbdcce 100644 --- a/documentation-site/firmware.json +++ b/documentation-site/firmware.json @@ -1,5 +1,5 @@ { - "version": "1.0.1-beta.5", + "version": "1.0.1-beta.6", "source_repository": "76cb/OpenTag-Station", "source_ref": "main", "acceptance": "pending" diff --git a/documentation-site/image-provenance.json b/documentation-site/image-provenance.json index 256e5f2..a30f5a5 100644 --- a/documentation-site/image-provenance.json +++ b/documentation-site/image-provenance.json @@ -9,7 +9,7 @@ "tools/capture_product_review.cjs": "23d6c11b616ac2c271e980410b162d71cf69b72575a452926794bf4838fe22d0", "tools/render_touch_review.py": "8cb523c706690f681f2dc887a726b21e99de020a4d8ec00a9708982073daeeac", "tools/check_public_privacy.py": "b5b3fc8482b8e9692e1524002b911cbcace8c213fd180454469f7c4963ebacd9", - "src/web/web_assets.cpp": "b565808d7de2cb95615d7ae49db614e4303aec28e91f5ae3ab8c61fa3eefb84c", + "src/web/web_assets.cpp": "61fdc3cd86d95272ab217b29c5f81fde6290625a97c33a5986a96bbb14bc962d", "src/web/writer_assets.cpp": "18ca31fb929c43f13138fbd4839e4ed60dc10e342cc2c0dbabb6e95d4c8a48c7", "src/web/writer_layout.inc": "c8835b92d1951269254fec884fe210fc725060528e5505e18a207fbe23d67f98", "src/ui/product_layout.hpp": "4b3139a7f8e95142f53b72a4c9f4ac05d103426a29210885823b30b3171c8979", @@ -18,10 +18,10 @@ "test/test_touch_flow/fixtures.hpp": "e895bf1032a0ddb04a71178fdc8ac3169fb3457274c376ff3e5bbf333f4efe1a" }, "images": { - "docs/assets/images/browser/dashboard.png": "5a532d455b3d67bd8199043b2b11bb1deba036ae776484082fcac62fd14aace5", - "docs/assets/images/browser/weigh.png": "2a2b2b70419ec4038aa37ef2ad630ef77f13cf2230f444a684805f1750df6bc4", - "docs/assets/images/browser/assignment.png": "60dcdb03a77d12639f0964a8fc2d26d441a9e4c4aa7fb51ae7f7af53387a1362", - "docs/assets/images/browser/tag-management.png": "41772e6e29be1725052d1df0e352fa874850da612c915ed112d030536057df1c", + "docs/assets/images/browser/dashboard.png": "09157492a3d145f557dac3cec7c1924fa30b41f875b3e5943d1c04d455c9fa26", + "docs/assets/images/browser/weigh.png": "9c961a746f6d6063e5a9c894c2736bbfdb5ae6caa0cacef28267bf55b2d3f47b", + "docs/assets/images/browser/assignment.png": "6d8683ed09f8829383c39a52a470df7741235694fb81132e0d1cfd8c9149543c", + "docs/assets/images/browser/tag-management.png": "d32cc3e87a3c3191c63a2cc2a6f834559380e63ed53186e87376eaa2305bcfae", "docs/assets/images/touchscreen/wt32-home.png": "c768e1b3be568f2563bdf0a368f2cf5b4aab2e2e6efb8a98c8a996ff4dcdc7e3" } } diff --git a/src/boards/wt32_sc01_plus_rev_a.hpp b/src/boards/wt32_sc01_plus_rev_a.hpp index c6314db..7f25786 100644 --- a/src/boards/wt32_sc01_plus_rev_a.hpp +++ b/src/boards/wt32_sc01_plus_rev_a.hpp @@ -35,6 +35,8 @@ struct Wt32Sc01PlusRevA { static constexpr std::uint32_t lcd_write_frequency_hz = 40000000U; static constexpr std::uint32_t touch_frequency_hz = 400000U; static constexpr std::uint8_t display_rotation = 1; + // The same landscape orientation turned by 180 degrees (LovyanGFX rotation index). + static constexpr std::uint8_t display_rotation_flipped = 3; static constexpr bool display_invert = true; static constexpr bool display_rgb_order = false; // LVGL emits native-endian RGB565 (LV_COLOR_16_SWAP=0). LovyanGFX must diff --git a/src/config/configuration_service.cpp b/src/config/configuration_service.cpp index c13e5f4..e8dd4ac 100644 --- a/src/config/configuration_service.cpp +++ b/src/config/configuration_service.cpp @@ -186,6 +186,7 @@ void write_known(JsonDocument& document, const Configuration& configuration) { auto device = object_at(document, "device"); device["hostname"] = configuration.device.hostname; device["brightness_percent"] = configuration.device.brightness_percent; + device["display_rotation"] = configuration.device.display_rotation; device["dim_after_ms"] = configuration.device.dim_after_ms; device["sleep_after_ms"] = configuration.device.sleep_after_ms; device["update_channel"] = configuration.device.update_channel; @@ -320,6 +321,8 @@ core::Result read_configuration( result.device.hostname = text_or(device["hostname"], result.device.hostname); result.device.brightness_percent = number_or(device["brightness_percent"], result.device.brightness_percent); + result.device.display_rotation = + number_or(device["display_rotation"], result.device.display_rotation); result.device.dim_after_ms = number_or(device["dim_after_ms"], result.device.dim_after_ms); result.device.sleep_after_ms = @@ -572,6 +575,7 @@ core::Result Configuration::validate() const { } if (!valid_hostname(device.hostname) || device.brightness_percent < 5U || device.brightness_percent > 100U || device.dim_after_ms == 0U || + (device.display_rotation != 0U && device.display_rotation != 180U) || device.sleep_after_ms < device.dim_after_ms || (device.update_channel != "stable" && device.update_channel != "beta" && device.update_channel != "development")) { diff --git a/src/config/configuration_service.hpp b/src/config/configuration_service.hpp index e4db631..5f4d86a 100644 --- a/src/config/configuration_service.hpp +++ b/src/config/configuration_service.hpp @@ -25,6 +25,8 @@ namespace detail { struct DeviceSettings { std::string hostname{"opentag-station"}; std::uint8_t brightness_percent{80U}; + // 0 or 180 degrees: the screen can be mounted upside down (printed case variants). + std::uint16_t display_rotation{0U}; std::uint32_t dim_after_ms{120000U}; std::uint32_t sleep_after_ms{300000U}; std::string update_channel{"stable"}; diff --git a/src/hardware/display/wt32_display.cpp b/src/hardware/display/wt32_display.cpp index d90180d..99f201e 100644 --- a/src/hardware/display/wt32_display.cpp +++ b/src/hardware/display/wt32_display.cpp @@ -81,7 +81,8 @@ bool Wt32Display::initialize() { if (!initialized_) { return false; } - device_.setRotation(Board::display_rotation); + device_.setRotation(rotation_degrees_ == 180U ? Board::display_rotation_flipped + : Board::display_rotation); device_.setColorDepth(16); device_.setSwapBytes(Board::display_swap_bytes); device_.setBrightness(static_cast(brightness_percent_ * 255U / 100U)); @@ -92,6 +93,15 @@ bool Wt32Display::initialize() { return initialized_; } +void Wt32Display::set_rotation(std::uint16_t degrees) { + rotation_degrees_ = degrees == 180U ? 180U : 0U; + if (initialized_) { + // LovyanGFX turns both the panel and the touch coordinates. + device_.setRotation(rotation_degrees_ == 180U ? Board::display_rotation_flipped + : Board::display_rotation); + } +} + void Wt32Display::set_brightness(std::uint8_t percent) { brightness_percent_ = std::min(percent, 100U); if (initialized_ && !sleeping_) { diff --git a/src/hardware/display/wt32_display.hpp b/src/hardware/display/wt32_display.hpp index cf25f93..195a95c 100644 --- a/src/hardware/display/wt32_display.hpp +++ b/src/hardware/display/wt32_display.hpp @@ -30,6 +30,8 @@ class Wt32Display { public: bool initialize(); void set_brightness(std::uint8_t percent); + // 0 or 180 degrees. Applies at once when the display runs; touch follows. + void set_rotation(std::uint16_t degrees); void sleep(); void wake(); void push_pixels( @@ -44,6 +46,7 @@ class Wt32Display { [[nodiscard]] bool sleeping() const { return sleeping_; } [[nodiscard]] bool touch_configured() const { return initialized_; } [[nodiscard]] std::uint8_t brightness() const { return brightness_percent_; } + [[nodiscard]] std::uint16_t rotation() const { return rotation_degrees_; } private: static constexpr std::uint32_t touch_debounce_ms = 20U; @@ -52,6 +55,7 @@ class Wt32Display { bool initialized_{false}; bool sleeping_{false}; std::uint8_t brightness_percent_{80}; + std::uint16_t rotation_degrees_{0}; bool touch_candidate_{false}; bool touch_stable_{false}; std::uint32_t touch_candidate_since_ms_{0}; diff --git a/src/ui/ui_service.cpp b/src/ui/ui_service.cpp index bcf241b..a1bbb24 100644 --- a/src/ui/ui_service.cpp +++ b/src/ui/ui_service.cpp @@ -237,6 +237,8 @@ bool UiService::initialize() { const auto configured = configuration_.snapshot(); normal_brightness_percent_ = configured.device.brightness_percent; display_.set_brightness(normal_brightness_percent_); + display_rotation_ = configured.device.display_rotation; + display_.set_rotation(display_rotation_); dim_after_ms_ = configured.device.dim_after_ms; sleep_after_ms_ = configured.device.sleep_after_ms; display_revision_ = configuration_.revision(); @@ -330,11 +332,20 @@ void UiService::apply_display_settings() { if (revision == display_revision_) return; display_revision_ = revision; std::uint8_t brightness = normal_brightness_percent_; + std::uint16_t rotation = display_rotation_; configuration_.visit([&](const config::Configuration& current, std::uint64_t) { brightness = current.device.brightness_percent; + rotation = current.device.display_rotation; dim_after_ms_ = current.device.dim_after_ms; sleep_after_ms_ = current.device.sleep_after_ms; }); + // A screen orientation saved in the browser turns the touchscreen at once; + // the whole screen is drawn again in the new orientation. + if (rotation != display_rotation_) { + display_rotation_ = rotation; + display_.set_rotation(rotation); + lv_obj_invalidate(lv_scr_act()); + } const bool dragging = brightness_slider_ != nullptr && lv_obj_has_state(brightness_slider_, LV_STATE_PRESSED); if (brightness == normal_brightness_percent_ || brightness_save_pending_ || dragging) return; diff --git a/src/ui/ui_service.hpp b/src/ui/ui_service.hpp index cad6532..0c0e4ec 100644 --- a/src/ui/ui_service.hpp +++ b/src/ui/ui_service.hpp @@ -200,6 +200,7 @@ class UiService { bool showing_diagnostics_{false}; bool showing_display_self_test_{OPENTAG_DISPLAY_SELF_TEST == 1}; std::uint8_t normal_brightness_percent_{80U}; + std::uint16_t display_rotation_{0U}; std::uint32_t dim_after_ms_{dim_after_ms}; ProductPage active_page_{ProductPage::home}; std::uint32_t sleep_after_ms_{sleep_after_ms}; diff --git a/src/web/api_router.cpp b/src/web/api_router.cpp index f18caf1..44bd308 100644 --- a/src/web/api_router.cpp +++ b/src/web/api_router.cpp @@ -656,15 +656,21 @@ core::Result parse_configuration_patch( const auto object = root["device"].as(); DevicePatch patch; if (object.size() == 0U || - !keys_allowed(object, {"hostname", "brightness_percent", "dim_after_ms", "sleep_after_ms", "update_channel"}) || + !keys_allowed(object, {"hostname", "brightness_percent", "display_rotation", "dim_after_ms", "sleep_after_ms", "update_channel"}) || !read_optional_string(object, "hostname", 63U, patch.hostname, false) || !read_optional_unsigned(object, "brightness_percent", 5U, 100U, patch.brightness_percent) || + !read_optional_unsigned(object, "display_rotation", 0U, 180U, patch.display_rotation) || !read_optional_unsigned(object, "dim_after_ms", 1U, 86400000U, patch.dim_after_ms) || !read_optional_unsigned(object, "sleep_after_ms", 1U, 86400000U, patch.sleep_after_ms) || !read_optional_string(object, "update_channel", 16U, patch.update_channel, false)) { return core::Result::failure( invalid_request("device patch contains invalid fields or values")); } + if (patch.display_rotation.has_value() && *patch.display_rotation != 0U && + *patch.display_rotation != 180U) { + return core::Result::failure( + invalid_request("device display rotation must be 0 or 180")); + } if (patch.update_channel.has_value() && *patch.update_channel != "stable" && *patch.update_channel != "beta" && *patch.update_channel != "development") { return core::Result::failure( diff --git a/src/web/api_router.hpp b/src/web/api_router.hpp index d7ac490..0f7516e 100644 --- a/src/web/api_router.hpp +++ b/src/web/api_router.hpp @@ -197,6 +197,7 @@ struct ToolheadUnassignmentMutation { struct DevicePatch { std::optional hostname; std::optional brightness_percent; + std::optional display_rotation; // 0 or 180 std::optional dim_after_ms; std::optional sleep_after_ms; std::optional update_channel; diff --git a/src/web/application_api_context.cpp b/src/web/application_api_context.cpp index ce1bedb..168f457 100644 --- a/src/web/application_api_context.cpp +++ b/src/web/application_api_context.cpp @@ -453,6 +453,7 @@ void write_configuration( auto device = document["device"].to(); device["hostname"] = value.device.hostname; device["brightness_percent"] = value.device.brightness_percent; + device["display_rotation"] = value.device.display_rotation; device["dim_after_ms"] = value.device.dim_after_ms; device["sleep_after_ms"] = value.device.sleep_after_ms; device["update_channel"] = value.device.update_channel; diff --git a/src/web/configuration_patch.cpp b/src/web/configuration_patch.cpp index 8a99be7..462048d 100644 --- a/src/web/configuration_patch.cpp +++ b/src/web/configuration_patch.cpp @@ -69,6 +69,9 @@ core::Result apply_configuration_patch( if (value.brightness_percent.has_value()) { proposed.device.brightness_percent = *value.brightness_percent; } + if (value.display_rotation.has_value()) { + proposed.device.display_rotation = *value.display_rotation; + } if (value.dim_after_ms.has_value()) { proposed.device.dim_after_ms = *value.dim_after_ms; } diff --git a/src/web/ui/product.js b/src/web/ui/product.js index 73766ce..e0e8fc1 100644 --- a/src/web/ui/product.js +++ b/src/web/ui/product.js @@ -227,7 +227,7 @@ function buildSettings() { root.addEventListener('change',markConfigDirty); // Dynamically created profile fields remain part of the optimistic settings form. const station=fields.find(f=>f.querySelector('#config-brightness')); - if(station){const brightness=byId('config-brightness'),label=station.querySelector('label[for="config-brightness"]');panes.display.replaceChildren(productElement('h3','Touchscreen display'),productElement('p','Set a comfortable brightness for the station.','muted'),label,brightness);} + if(station){const brightness=byId('config-brightness'),label=station.querySelector('label[for="config-brightness"]'),rotation=byId('config-rotation'),rotationLabel=station.querySelector('label[for="config-rotation"]');panes.display.replaceChildren(productElement('h3','Touchscreen display'),productElement('p','Set a comfortable brightness for the station. Turn the screen upside down if the case holds the board that way.','muted'),label,brightness,rotationLabel,rotation);} selectSettings('station'); } function bindProduct() { diff --git a/src/web/web_assets.cpp b/src/web/web_assets.cpp index 987970b..3db8b4f 100644 --- a/src/web/web_assets.cpp +++ b/src/web/web_assets.cpp @@ -163,6 +163,7 @@ const char index_html[] = R"HTML(

Device +
Wi-Fi
Not scanned yet
@@ -1935,6 +1936,7 @@ applyAuthState(web.access_token_configured === true, state.configRevision); const profile = asObject(first(payload.scale_profile, payload.scale && payload.scale.profile, {})); setValue('config-hostname', device.hostname); setValue('config-brightness', device.brightness_percent); +setValue('config-rotation', String(first(device.display_rotation, 0))); setValue('config-ssid', wifi.ssid); setValue('config-spoolman-url', spoolman.url); setValue('config-filabridge-url', filabridge.url); @@ -2023,7 +2025,7 @@ function configPatch() { const profileId = valueOf('config-scale-profile'); const patch = { expected_revision: Number(state.configRevision), -device: { hostname: valueOf('config-hostname'), brightness_percent: Number(valueOf('config-brightness')) }, +device: { hostname: valueOf('config-hostname'), brightness_percent: Number(valueOf('config-brightness')), display_rotation: Number(valueOf('config-rotation')) }, wifi: { ssid: rawValueOf('config-ssid') }, spoolman: { url: valueOf('config-spoolman-url') }, filabridge: { url: valueOf('config-filabridge-url'), selected_printer_id: rawValueOf('config-printer-id') }, @@ -3486,7 +3488,7 @@ function buildSettings() { root.addEventListener('change',markConfigDirty); // Dynamically created profile fields remain part of the optimistic settings form. const station=fields.find(f=>f.querySelector('#config-brightness')); - if(station){const brightness=byId('config-brightness'),label=station.querySelector('label[for="config-brightness"]');panes.display.replaceChildren(productElement('h3','Touchscreen display'),productElement('p','Set a comfortable brightness for the station.','muted'),label,brightness);} + if(station){const brightness=byId('config-brightness'),label=station.querySelector('label[for="config-brightness"]'),rotation=byId('config-rotation'),rotationLabel=station.querySelector('label[for="config-rotation"]');panes.display.replaceChildren(productElement('h3','Touchscreen display'),productElement('p','Set a comfortable brightness for the station. Turn the screen upside down if the case holds the board that way.','muted'),label,brightness,rotationLabel,rotation);} selectSettings('station'); } function bindProduct() { @@ -3649,7 +3651,7 @@ renderScale: renderScale, renderSpool: renderSpool, renderNfc: renderNfc, renderTag: renderTag, -renderDevice,renderNetwork,renderConfig,renderDiagnostics,renderLogs, +renderDevice,renderNetwork,renderConfig,configPatch,renderDiagnostics,renderLogs, productPageFromHash: productPageFromHash, activateProductPage: activateProductPage, navigateProductPage: navigateProductPage, diff --git a/test/test_configuration/test_main.cpp b/test/test_configuration/test_main.cpp index d6269db..89e3696 100644 --- a/test/test_configuration/test_main.cpp +++ b/test/test_configuration/test_main.cpp @@ -694,6 +694,36 @@ void test_initialize_revision_changes_only_on_successful_load() { TEST_ASSERT_EQUAL_UINT64(2U, service.revision()); } +// 1.0.1-beta.6: the screen orientation is a saved setting, 0 or 180 degrees. +void test_display_rotation_defaults_to_zero_round_trips_and_is_validated() { + MemoryDocumentStore documents; + LegacyScaleStore legacy; + ConfigurationService service(documents, legacy); + TEST_ASSERT_TRUE(service.initialize().ok()); + TEST_ASSERT_EQUAL_UINT16(0U, service.snapshot().device.display_rotation); + auto turned = service.snapshot(); + turned.device.display_rotation = 180U; + TEST_ASSERT_TRUE(service.replace(turned).ok()); + TEST_ASSERT_EQUAL_UINT16(180U, service.snapshot().device.display_rotation); + TEST_ASSERT_NOT_EQUAL(std::string::npos, documents.document->find("\"display_rotation\":180")); + // Saved settings are read back with the rotation. + ConfigurationService reloaded(documents, legacy); + TEST_ASSERT_TRUE(reloaded.initialize().ok()); + TEST_ASSERT_EQUAL_UINT16(180U, reloaded.snapshot().device.display_rotation); + // Only 0 and 180 are accepted; a document without the key means 0. + Configuration configuration; + configuration.device.display_rotation = 90U; + TEST_ASSERT_FALSE(configuration.validate().ok()); + configuration.device.display_rotation = 180U; + TEST_ASSERT_TRUE(configuration.validate().ok()); + MemoryDocumentStore older; + older.document = schema_one_document; + LegacyScaleStore older_legacy; + ConfigurationService migrated(older, older_legacy); + TEST_ASSERT_TRUE(migrated.initialize().ok()); + TEST_ASSERT_EQUAL_UINT16(0U, migrated.snapshot().device.display_rotation); +} + void test_validation_rejects_invalid_urls_and_duplicate_toolheads() { Configuration configuration; configuration.spoolman.url = "ftp://not-supported"; @@ -1344,6 +1374,7 @@ int main(int, char**) { RUN_TEST(test_initialize_revision_changes_only_on_successful_load); RUN_TEST(test_validation_rejects_invalid_urls_and_duplicate_toolheads); RUN_TEST(test_validation_rejects_invalid_scale_hardware_settings); + RUN_TEST(test_display_rotation_defaults_to_zero_round_trips_and_is_validated); RUN_TEST(test_web_access_token_validation_is_fail_closed_and_ascii_only); RUN_TEST(test_local_interface_snapshot_is_narrow_current_and_independent); RUN_TEST(test_scale_profile_snapshot_is_scoped_revisioned_and_independent); diff --git a/test/test_ui_contract/test_main.cpp b/test/test_ui_contract/test_main.cpp index 8efc74e..ddf41f2 100644 --- a/test/test_ui_contract/test_main.cpp +++ b/test/test_ui_contract/test_main.cpp @@ -426,6 +426,16 @@ void test_rc23_brightness_is_saved_and_follows_settings() { for (const char* token : {"dim_after_ms_ = current.device.dim_after_ms", "sleep_after_ms_ = current.device.sleep_after_ms", "lv_slider_set_value(brightness_slider_"}) TEST_ASSERT_TRUE_MESSAGE(apply.find(token) != std::string::npos, token); + // 1.0.1-beta.6: the saved screen orientation is applied at start and when + // it changes in the browser, with a full redraw; it is not gated by the + // brightness early return. + for (const char* token : {"rotation = current.device.display_rotation", "display_.set_rotation(rotation)", + "lv_obj_invalidate(lv_scr_act())"}) + TEST_ASSERT_TRUE_MESSAGE(apply.find(token) != std::string::npos, token); + TEST_ASSERT_LESS_THAN(apply.find("brightness == normal_brightness_percent_"), apply.find("display_.set_rotation(rotation)")); + TEST_ASSERT_TRUE(source.find("display_.set_rotation(display_rotation_)") != std::string::npos); + const auto driver = read_source("src/hardware/display/wt32_display.cpp"); + TEST_ASSERT_TRUE(driver.find("Board::display_rotation_flipped") != std::string::npos); } void test_rc23_touchscreen_wording_is_plain() { diff --git a/test/test_web_api/test_main.cpp b/test/test_web_api/test_main.cpp index ee232da..06e1268 100644 --- a/test/test_web_api/test_main.cpp +++ b/test/test_web_api/test_main.cpp @@ -817,6 +817,21 @@ void test_configuration_patch_is_typed_and_omitted_secrets_remain_omitted() { TEST_ASSERT_NULL(std::strstr(response.body.c_str(), "new-token")); } +// 1.0.1-beta.6: the screen orientation, 0 or 180, is a typed device field. +void test_configuration_patch_accepts_display_rotation() { + FakeContext context; + Router router(context); + const auto response = router.handle(mutation_request( + Method::patch, "/api/v1/config", + R"({"expected_revision":7,"device":{"display_rotation":180}})")); + TEST_ASSERT_EQUAL_INT(202, response.status); + const auto& patch = + std::get(context.last_mutation->payload); + TEST_ASSERT_TRUE(patch.device.has_value()); + TEST_ASSERT_EQUAL_UINT16(180U, *patch.device->display_rotation); + TEST_ASSERT_FALSE(patch.device->brightness_percent.has_value()); +} + void test_configuration_web_access_token_patch_is_strict() { FakeContext context; Router router(context); @@ -891,8 +906,10 @@ void test_configuration_web_access_token_patch_is_strict() { void test_configuration_patch_rejects_unknown_invalid_or_ambiguous_fields() { FakeContext context; Router router(context); - const std::array invalid = {{ + const std::array invalid = {{ R"({"device":{"hostname":"station"}})", + R"({"expected_revision":7,"device":{"display_rotation":90}})", + R"({"expected_revision":7,"device":{"display_rotation":"180"}})", R"({"expected_revision":7})", R"({"expected_revision":7,"wifi":{"password_configured":true}})", R"({"expected_revision":7,"scale_profile":{"id":"yzc-133-2kg","rated_capacity_grams":5000}})", @@ -1747,6 +1764,7 @@ int main(int, char**) { RUN_TEST(test_configuration_patch_is_typed_and_omitted_secrets_remain_omitted); RUN_TEST(test_configuration_web_access_token_patch_is_strict); RUN_TEST(test_configuration_patch_rejects_unknown_invalid_or_ambiguous_fields); + RUN_TEST(test_configuration_patch_accepts_display_rotation); RUN_TEST(test_configuration_snapshot_blocks_secret_keys_but_allows_flags); RUN_TEST(test_snapshot_json_must_be_valid_and_bounded); RUN_TEST(test_operation_status_has_strict_positive_ids_and_not_found_state); diff --git a/test/test_web_configuration/test_main.cpp b/test/test_web_configuration/test_main.cpp index 3d5e2f3..aa5d3e7 100644 --- a/test/test_web_configuration/test_main.cpp +++ b/test/test_web_configuration/test_main.cpp @@ -245,6 +245,7 @@ void test_all_typed_nonsecret_sections_are_applied() { opentag::web::api::DevicePatch device; device.brightness_percent = 55U; + device.display_rotation = 180U; device.dim_after_ms = 30000U; device.sleep_after_ms = 60000U; device.update_channel = "beta"; @@ -277,6 +278,7 @@ void test_all_typed_nonsecret_sections_are_applied() { TEST_ASSERT_TRUE(applied.ok()); const auto& result = applied.value(); TEST_ASSERT_EQUAL_UINT8(55U, result.device.brightness_percent); + TEST_ASSERT_EQUAL_UINT16(180U, result.device.display_rotation); TEST_ASSERT_FALSE(result.wifi.auto_reconnect); TEST_ASSERT_EQUAL_UINT32(20000U, result.wifi.connect_timeout_ms); TEST_ASSERT_EQUAL_STRING( diff --git a/tools/test_web_transport.mjs b/tools/test_web_transport.mjs index ebbefd5..277aa79 100644 --- a/tools/test_web_transport.mjs +++ b/tools/test_web_transport.mjs @@ -1670,6 +1670,25 @@ test('an NFC-A tag is named as not supported yet in Manage tag and on the Dashbo inventory: { tag_count: 2, present: true, technology: 'NFC-A / ISO14443A' } }); assert.equal(document.getElementById('nfc-summary').textContent, 'Multiple NFC-A tags detected'); }); +test('the screen orientation is a Settings → Display field that round-trips through the config patch', () => { + const { T, document } = loadApplication(); + T.bindProduct(); + T.renderConfig({ revision: 4, device: { hostname: 'opentag-station', brightness_percent: 80, display_rotation: 180 }, + wifi: { ssid: 'lab' }, spoolman: { url: 'http://spoolman.local:7912' }, filabridge: {}, web: { access_token_configured: false }, + scale_profile: { id: 'yzc-133-5kg', overload_ratio: 1.1 } }); + const rotation = document.getElementById('config-rotation'); + assert.equal(rotation.value, '180'); + assert.equal(String(document.getElementById('config-brightness').value), '80'); + // The control lives on the Display pane next to the brightness. + assert.equal(rotation.parentElement, document.getElementById('config-brightness').parentElement); + rotation.value = '0'; + assert.equal(T.configPatch().device.display_rotation, 0); + rotation.value = '180'; + assert.equal(T.configPatch().device.display_rotation, 180); + // A station that does not report the key (older firmware) shows Normal. + T.renderConfig({ revision: 5, device: { hostname: 'opentag-station', brightness_percent: 80 } }); + assert.equal(rotation.value, '0'); +}); test('product navigation exposes four product destinations and preserves legacy deep links', () => { const { T, document, context } = loadApplication(); const pages = ['home', 'inventory', 'printer', 'settings']; diff --git a/web-flasher/manifest.json b/web-flasher/manifest.json index 9a96ef9..f0cc04f 100644 --- a/web-flasher/manifest.json +++ b/web-flasher/manifest.json @@ -1,6 +1,6 @@ { "name": "OpenTag Station", - "version": "1.0.1-beta.5", + "version": "1.0.1-beta.6", "new_install_prompt_erase": true, "new_install_improv_wait_time": 0, "builds": [