Windows Urovo D812R+ print+encode daemon + full assessment
- pricegod-urovo-daemon: C# :7790 HTTP service, drives the Urovo D812R+ via the GTSPL SDK; prints a label AND encodes the UHF chip in one pass. Drop-in for the PriceGod extension's daemon.js (same :7790, CORS *). - chafcheck: Chafon H-102 UHFPrimeReader P/Invoke probe. - ASSESSMENT.md: everything done + learned this session -- the ribbon-latch fix, PET-labels-are-not-direct-thermal finding, unreliable-status-byte quirk, SKU->EPC scheme, the Chafon HID-mode dead end, and how to port the print+encode recipe to the Mac (GTSPL Java SDK / raw TSPL). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
3056fd70fc
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
# build outputs (rebuild with build.cmd)
|
||||
*.exe
|
||||
*.pdb
|
||||
*.obj
|
||||
|
||||
# editor/OS cruft
|
||||
.vs/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# NOTE: the vendor DLLs (GTSPL_SDK*.dll, zlib.net.dll, UHFPrimeReader.dll, hidapi.dll)
|
||||
# ARE committed on purpose — they're needed to build/run on Windows.
|
||||
199
ASSESSMENT.md
Normal file
199
ASSESSMENT.md
Normal file
@ -0,0 +1,199 @@
|
||||
# Urovo D812R+ RFID print+encode — full assessment & handover
|
||||
|
||||
_Session date: 2026-07-22 (Windows 11 bench machine). Goal: make the **Urovo D812R+ (RFID)**
|
||||
print a record label **and** program the UHF chip **in one pass**, driven from the PriceGod
|
||||
browser extension — replacing the current two-step flow (Chafon gun programs a chip → Dymo 450
|
||||
prints a paper label → stick paper over chip)._
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
- ✅ **Built a working Windows daemon** (`pricegod-urovo-daemon`, C#/.NET Framework, `http://localhost:7790`)
|
||||
that drives the Urovo via the **GTSPL SDK** and does **print + RFID-encode in one pass**. Drop-in
|
||||
for the extension's `daemon.js` (same port, same JSON, CORS `*`).
|
||||
- ✅ **Printing works** — dark, correct, on real thermal stock, at the right size (54×24). The daemon
|
||||
even **auto-clears the recurring "out of ribbon" latch** on every print.
|
||||
- ✅ **RFID read/encode path proven** — the Urovo reads UCODE-9 chips and the `writeUHF`+`printlabel`
|
||||
encode sequence is wired and fires cleanly.
|
||||
- ⛔ **The blocker to the actual one-pass dream is the LABELS, not the printer:** the shop's glossy
|
||||
**"U9 65×35mm PET"** RFID labels are **not direct-thermal** — a direct-thermal printer can't mark
|
||||
them (only a faint emboss). Need **direct-thermal UHF RFID _paper_ labels**.
|
||||
- ⛔ **Chafon H-102 on Windows is a dead end for _writing_** — it outputs as an HID keyboard, not
|
||||
serial; the SDK can't reach it to program tags without a chronically-unreliable device-side mode
|
||||
flip. Recommend: **Chafon = scanner (reads/looks-up), Urovo = all writing.**
|
||||
|
||||
---
|
||||
|
||||
## 1. What was built (in this repo)
|
||||
|
||||
| File | What |
|
||||
|---|---|
|
||||
| `daemon.cs` | The service. `HttpListener` on `:7790`, CORS `*`, drives the Urovo via `GTSPL_SDK.dll`. |
|
||||
| `chafcheck.cs` | Probe for the Chafon reader via native `UHFPrimeReader.dll` (P/Invoke). |
|
||||
| `start-daemon.cmd` / `build.cmd` | Run / rebuild (rebuild uses the always-present .NET Framework `csc.exe` — **no .NET SDK needed**). |
|
||||
| `README.md` | How to run + routes. |
|
||||
| `PROGRESS.md` | Running log. |
|
||||
| `*.dll` | Vendor binaries needed to build/run on Windows: `GTSPL_SDK.dll`, `GTSPL_SDK_C.dll`, `zlib.net.dll` (Urovo), `UHFPrimeReader.dll`, `hidapi.dll` (Chafon). |
|
||||
|
||||
Build with `build.cmd` (rebuilds `pricegod-urovo-daemon.exe`; `.exe`s are git-ignored). Run with
|
||||
`start-daemon.cmd`.
|
||||
|
||||
### Daemon routes (`:7790`, JSON)
|
||||
| method | path | body | returns |
|
||||
|---|---|---|---|
|
||||
| GET | `/status` | — | `{ok,ready,status,statusText,printer}` |
|
||||
| POST | `/print-encode` | `{sku,releaseId,artist?,title?,genre?,style?,info?,price?,condition?,w?,h?,gap?,dir?}` | `{ok,epc,status,note,...}` |
|
||||
| POST | `/write-tag` | alias of `/print-encode` (the daemon.js name) | same |
|
||||
| POST | `/print-test` | same body, **prints only, no RFID** (safe on plain thermal) | — |
|
||||
| GET | `/read-tag` / `/read-raw` | — | tag EPC / all memory banks |
|
||||
| POST | `/calibrate` / `/recover` | — | RFID auto-cal / ribbon-off+feed clear |
|
||||
|
||||
---
|
||||
|
||||
## 2. The proven Urovo print+encode recipe (⭐ portable — this is what transfers to the Mac)
|
||||
|
||||
Every step below is the **exact GTSPL SDK call sequence** the daemon uses. Almost all of them are
|
||||
plain **TSPL commands** (`sendcommand`) — the only special one is the RFID write. This recipe is
|
||||
device knowledge, not language knowledge; it should replicate on _any_ GTSPL binding (Windows .NET,
|
||||
**Java/`GTSPL-1.1.8.jar`**, Android, etc.):
|
||||
|
||||
```
|
||||
detectUSB / openport # connect to "Urovo D812Riplus(RFID)"
|
||||
SET RIBBON OFF # direct thermal (no ribbon). REQUIRED, repeatedly (see §3)
|
||||
<if status != "00">: formfeed; SET RIBBON OFF # clear a latched "out of ribbon"
|
||||
SET TEAR ON
|
||||
GAP 2 mm,0 mm # gapped die-cut labels (Dymo 54×24). For RFID rolls that
|
||||
# blind the gap sensor: GAP 0 mm,0 mm (continuous)
|
||||
SIZE 54 mm,24 mm # label size (was 65×35 for the PET RFID stock)
|
||||
DENSITY 15 # MAX darkness — needed; lower barely marks
|
||||
DIRECTION 1 # flip to 0 if the label prints rotated
|
||||
CLS (clearbuffer)
|
||||
writeUHF("H", 2, 12, "E", <24-hex EPC>) # ⭐ THE RFID ENCODE: hex, EPC bank ("E"), word 2, 12 bytes (96-bit EPC)
|
||||
TEXT/QRCODE ... (printerfont/qrcode) # the visible label
|
||||
SET RIBBON OFF # re-assert immediately before printing
|
||||
printlabel("1","1") # fires PRINT + ENCODE together, one pass
|
||||
<wait for status to return to "00"> # RFID+feed leaves it "busy" for ~1–3s (see §3)
|
||||
readUHF("H",2,12,"E") / query_UHF # optional verify (note: the encoded tag has fed past the
|
||||
# antenna by now, so on-printer read-back is unreliable —
|
||||
# verify with a separate reader / Chafon scan instead)
|
||||
```
|
||||
|
||||
**SKU ↔ EPC scheme (v1, in the `Epc` class):** the 96-bit EPC (12 bytes) = **6 bytes** 14-digit
|
||||
timestamp SKU + **4 bytes** release_id + **2 bytes** `0xEC01` marker. ⚠️ **Unverified against the
|
||||
Mac/Chafon scheme** — if existing tags use a different layout, change ONLY the `Epc` class; nothing
|
||||
else depends on the byte order. (Best check: scan an existing Chafon-written tag and compare.)
|
||||
|
||||
---
|
||||
|
||||
## 3. Hard-won gotchas (the Urovo D812R+ specifically)
|
||||
|
||||
1. **PET labels aren't direct-thermal.** The glossy **"U9 65×35mm PET"** RFID labels only emboss —
|
||||
they will _never_ print dark on a direct-thermal printer. This cost hours (looked like a
|
||||
density/ribbon/printer fault; it was the media). Scratch test: drag a nail across a blank label —
|
||||
direct-thermal paper leaves a gray streak, PET/plain leaves nothing. **Buy direct-thermal RFID
|
||||
_paper_ labels** (see §5).
|
||||
2. **"Out of ribbon" (status `08`) is a latch.** The printer defaults to thermal-transfer (expects a
|
||||
ribbon). `SET RIBBON OFF` switches to direct thermal — but a _latched_ `08` (e.g. after opening
|
||||
the head / pulling the roll back) is **NOT** cleared by `SET RIBBON OFF` alone; it needs a
|
||||
**power cycle**, OR a **formfeed + SET RIBBON OFF** (which the daemon now does automatically on a
|
||||
non-`00` start).
|
||||
3. **The status byte lies right after a print.** During the RFID-encode + feed the reader returns
|
||||
garbage codes (`20`, `31`, `45`, `FC`) for 1–3s, then settles to `00`. Do **not** judge success on
|
||||
the immediate post-print byte — poll until `00` (the daemon's `WaitReady`), and verify the actual
|
||||
result by eye / a separate tag read.
|
||||
4. **USB drops on power-off.** Powering the printer off removes it from the USB bus; it re-enumerates
|
||||
a few seconds after power-on. If `detectUSB` returns null/`no printer`, wait or replug. (COM4/COM5
|
||||
on this laptop are the Urovo's _Bluetooth_ SPP links — ignore them.)
|
||||
5. **Label-size calibration is on the printer:** hold the **FEED** button while powering on, wait for
|
||||
**5 beeps** — it auto-calibrates gap/label size. Needed after changing stock.
|
||||
6. **Talk to it via the GTSPL SDK, not the Windows spooler.** Raw TSPL through the Seagull print
|
||||
driver/spooler did not reliably deliver commands (esp. `SET RIBBON OFF`); the SDK's direct USB path
|
||||
does. Build without a .NET SDK using `Framework64\v4.0.30319\csc.exe`.
|
||||
|
||||
---
|
||||
|
||||
## 4. The Chafon H-102 situation (why it's parked)
|
||||
|
||||
The two CF-H102 guns, on Windows, put their tag reads on the **HID keyboard** channel (USB-HID, or
|
||||
BLE "LE GATT HID device" when paired) — **not** the CH340 serial the SDK needs. Proven: `chafcheck`
|
||||
(`OpenDevice`+`GetTagUii`) returns `0xFFFFFF12` (`COMM_TIMEOUT`, "reader not answering") at **every**
|
||||
baud on **both** guns; a raw serial read with **DTR+RTS asserted** got **0 bytes** while triggering.
|
||||
|
||||
To _write_ tags via the SDK the gun must be in **serial/command mode**, flipped **device-side**
|
||||
(config barcode / the sideloaded Android app) — which the user reports is chronically flaky (Mac too).
|
||||
The Windows Chafon SDK has serial / USB-HID / network paths only — **no BLE** — so Bluetooth isn't a
|
||||
path for the gun here either.
|
||||
|
||||
**Recommendation:** stop fighting it. Use the Chafon as a **keyboard scanner** (trigger → _types_ the
|
||||
EPC → focus a search box to look up a tag; 100% reliable in the mode it's already in) and let the
|
||||
**Urovo do all writing**. Once direct-thermal RFID labels arrive, the Chafon isn't needed to program
|
||||
anything.
|
||||
|
||||
---
|
||||
|
||||
## 5. What to buy (unblocks the one-pass dream)
|
||||
|
||||
**Direct-thermal UHF RFID _paper_ labels**, all of:
|
||||
- **Direct thermal** (⚠ not "thermal transfer", not "PET/PP") — search **"direct thermal RFID label"**
|
||||
- **Paper face** (direct-thermal coated paper)
|
||||
- **UHF RFID inlay, EPC Gen2, chip NXP UCODE 9** (matches existing tags/reader)
|
||||
- **54×24 mm** (or 55×25 — the Dymo size), roll compatible with the D812R
|
||||
- Ask the seller: **"compatible with Urovo/TSC D812R RFID printer"**
|
||||
|
||||
The existing PET tags aren't wasted — a reader can still _program_ them; you'd just keep
|
||||
printing+sticking paper for those. New DT-RFID paper labels are what enable single-pass.
|
||||
|
||||
---
|
||||
|
||||
## 6. Two-mode design (agreed)
|
||||
|
||||
- **Mode A — "Separate" (today's flow, existing stock):** program chip + print Dymo separately, stick
|
||||
paper on top. On Windows the encode step is the flaky Chafon (see §4) — recommend leaning on the
|
||||
Urovo instead where possible.
|
||||
- **Mode B — "Combined / one-pass" (the goal):** one button → Urovo prints + encodes together on
|
||||
DT-RFID labels. Daemon path (`/print-encode`) is built.
|
||||
|
||||
A Settings toggle picks the mode; the extension change is tiny (same `daemon.js` contract). Wiring the
|
||||
PriceGod buttons → `:7790` was not done yet (next step).
|
||||
|
||||
---
|
||||
|
||||
## 7. ⭐ For the Mac: getting the Urovo to program chips over there
|
||||
|
||||
The **daemon.cs is Windows-bound** — `GTSPL_SDK.dll` is a Windows DLL; it won't run on macOS. But the
|
||||
**recipe in §2 is portable**. Options, best first:
|
||||
|
||||
1. **GTSPL Java SDK (most direct).** The vendor SDK ships a cross-platform Java library:
|
||||
`Documents/Software V1.1.2/drive-download-.../20241129_GTSPL_Java_Library/lib/GTSPL-1.1.8.jar`
|
||||
(also `gtspl_sdk.jar` in the Android samples). Java runs on macOS — write a small program that
|
||||
mirrors §2 (`openport`/`sendcommand`/`writeUHF`/`printlabel`). ⚠ Verify the jar's USB transport
|
||||
works on macOS (the Android build assumes Android USB; desktop USB may need a native lib or a
|
||||
serial/USB-CDC path). This is the cleanest port because `writeUHF` does the RFID encoding for you.
|
||||
2. **Raw TSPL over USB (no SDK).** ~90% of §2 is plain TSPL text you can send straight to the Urovo's
|
||||
USB device on macOS (e.g. write to the raw printer device, or CUPS `-o raw`). The **one missing
|
||||
piece is the RFID-encode command** — `writeUHF("H",2,12,"E",epc)` is an SDK helper wrapping the
|
||||
printer's TSPL RFID command, and its exact bytes weren't captured this session. To get them: sniff
|
||||
the USB writes while `writeUHF` runs on Windows (USBPcap/Wireshark), or check the Urovo/Gainscha
|
||||
TSPL RFID command reference. Once you have that command string, the whole thing is raw TSPL and
|
||||
trivially portable (even into the existing Node `rfid-daemon`).
|
||||
3. **Extend the existing Mac `rfid-daemon`** (Node) to open the Urovo and emit the §2 sequence
|
||||
(needs the RFID command bytes from option 2).
|
||||
|
||||
**Reality check for "can the Urovo program chips on Mac?":** yes, mechanically — it's the same USB
|
||||
printer speaking TSPL. The only real work is the transport (get bytes to the printer on macOS) and the
|
||||
RFID-encode command (use the Java `writeUHF`, or capture its bytes). Everything else — ribbon-off,
|
||||
geometry, density, one-pass timing — is documented above and identical.
|
||||
|
||||
---
|
||||
|
||||
## 8. Open items / next steps
|
||||
- [ ] Buy direct-thermal UHF RFID paper labels (§5) — the true unblock.
|
||||
- [ ] Verify the SKU↔EPC scheme (§2) against an existing tag; adjust `Epc` class if needed.
|
||||
- [ ] Wire PriceGod buttons → `:7790` + the Mode A/B toggle.
|
||||
- [ ] Match the label layout to the extension's `labels.js` design more faithfully (current TSPL layout is functional, not pixel-perfect).
|
||||
- [ ] (Optional) On Windows, decide Chafon = scanner-only vs. chase command mode.
|
||||
- [ ] (Mac) Try option 1 or 2 in §7 to program chips there.
|
||||
|
||||
See also the shop's memory notes: `urovo-d812r-ribbon-fix`, `urovo-d812r-rfid-label-gap-issue`,
|
||||
`urovo-d812r-pet-labels-not-thermal`, `urovo-windows-print-encode-daemon`, `chafon-h102-windows-hid-mode`.
|
||||
BIN
GTSPL_SDK.dll
Normal file
BIN
GTSPL_SDK.dll
Normal file
Binary file not shown.
BIN
GTSPL_SDK_C.dll
Normal file
BIN
GTSPL_SDK_C.dll
Normal file
Binary file not shown.
24
PROGRESS.md
Normal file
24
PROGRESS.md
Normal file
@ -0,0 +1,24 @@
|
||||
# Urovo one-pass print+encode — progress log
|
||||
|
||||
## Working
|
||||
- Daemon builds/runs: `pricegod-urovo-daemon.exe` on :7790 (start-daemon.cmd).
|
||||
- Talks to Urovo via GTSPL SDK (bypasses Windows spooler — NOT driver hell).
|
||||
- /status, /read-tag, /read-raw, /print-test (thermal only), /print-encode, /recover all wired.
|
||||
- RFID reads work: fresh UCODE 9 tags read EPC E28069950000600625F60A2A at antenna.
|
||||
- SKU<->EPC packing v1 in Epc class (6B sku + 4B releaseId + EC01 marker). NOT yet verified vs Chafon.
|
||||
|
||||
## Diagnosed
|
||||
- Print WAS happening but DENSITY 10 = almost invisible. Bumped DEF_DENSITY -> 15.
|
||||
- "VOID0" stamps = RFID encode voided because no tag was seated at antenna.
|
||||
- Status byte is UNRELIABLE right after a print (returns garbage 45/FC); reliable (00) only when idle.
|
||||
=> success must be judged by eyes / Chafon read-back, not the post-print status byte.
|
||||
- BLOCKER: printer latches "08 Out of ribbon" (thermal-transfer mode) at printlabel time.
|
||||
SET RIBBON OFF alone does NOT clear the latch (tested via gtcheck cmd, 0 tags).
|
||||
Next: hardware reset (re-seat head + power cycle). If it re-latches from fresh power-on,
|
||||
find the PERSISTENT direct-thermal media command in the GTSPL reference PDF.
|
||||
|
||||
## Still to do
|
||||
- Kill the ribbon latch so a dark label prints.
|
||||
- Verify encode by scanning the printed tag with the Chafon.
|
||||
- Match the 51x19 label layout from the extension's labels.js (current daemon layout is functional only).
|
||||
- Wire daemon.js / the HUD 'Write'/'Label' buttons to /print-encode.
|
||||
54
README.md
Normal file
54
README.md
Normal file
@ -0,0 +1,54 @@
|
||||
# pricegod-urovo-daemon
|
||||
|
||||
The **Windows** counterpart to the Mac `rfid-daemon`. Instead of a Chafon gun writing a
|
||||
loose chip while a Dymo prints a separate sticker, this drives the **Urovo D812R+ (RFID)**
|
||||
to **print the label AND encode the UHF chip in one pass** on the composite (paper+chip) rolls.
|
||||
|
||||
It listens on `http://localhost:7790/` with `CORS *`, so it's a drop-in for the PriceGod
|
||||
extension's existing `daemon.js` client — same port, same JSON shape.
|
||||
|
||||
## Run it
|
||||
Double-click **`start-daemon.cmd`** (leave the window open while you work). That's it —
|
||||
no Node, no .NET SDK, no gitea clone. It's a self-contained `.exe`.
|
||||
|
||||
To rebuild after editing `daemon.cs`: double-click **`build.cmd`**.
|
||||
|
||||
## What's inside
|
||||
- `daemon.cs` — the service (C#, .NET Framework 4.x).
|
||||
- `pricegod-urovo-daemon.exe` — the compiled daemon.
|
||||
- `GTSPL_SDK.dll`, `GTSPL_SDK_C.dll`, `zlib.net.dll` — the Gainscha/Urovo print SDK
|
||||
(must sit beside the .exe). Copied from the vendor Windows SDK.
|
||||
|
||||
The print/encode sequence is a straight port of the proven `gtcheck.cs` `rfidgo` path:
|
||||
`SET RIBBON OFF` → continuous geometry (`GAP 0`, RFID rolls blind the optical gap sensor)
|
||||
→ `writeUHF` → draw text + QR → `printlabel` → read-back verify.
|
||||
|
||||
## Routes
|
||||
| method | path | body | returns |
|
||||
|---|---|---|---|
|
||||
| GET | `/status` | — | `{ok, ready, status, statusText, printer}` |
|
||||
| POST | `/print-encode` | `{sku, releaseId, artist?, title?, price?, condition?, w?, h?, gap?}` | `{ok, sku, releaseId, epc, verified, status}` |
|
||||
| POST | `/write-tag` | alias of `/print-encode` (the daemon.js name) | same |
|
||||
| GET | `/read-tag` | — | `{ok, epc, sku, releaseId, recognized}` |
|
||||
| POST | `/calibrate` | — | RFID auto-calibration |
|
||||
| POST | `/recover` | — | ribbon-off + one feed to clear a fault |
|
||||
|
||||
Quick test (PowerShell):
|
||||
```powershell
|
||||
Invoke-RestMethod http://localhost:7790/status
|
||||
```
|
||||
|
||||
## SKU ↔ EPC scheme (⚠ verify)
|
||||
The 96-bit EPC (12 bytes) is packed as: **6 bytes** 14-digit timestamp SKU · **4 bytes**
|
||||
release_id · **2 bytes** `0xEC01` marker. This lives in the `Epc` class in `daemon.cs` and
|
||||
is the **only** thing that needs to change if it must match a different (Mac/Chafon) layout —
|
||||
nothing else depends on the byte order. Confirm by reading an existing tag: put a
|
||||
Chafon-written record at the antenna and `GET /read-tag`.
|
||||
|
||||
## Notes / open items
|
||||
- Default label geometry is `65×37 mm`, continuous (`GAP 0`). Adjust to your actual RFID
|
||||
roll via the `w`/`h`/`gap` fields, or change the `DEF_*` constants.
|
||||
- The label layout (text lines + QR) is functional, not yet pixel-matched to the 51×19
|
||||
Dymo design in the extension's `labels.js`.
|
||||
- The Chafon gun is still needed for **reading a loose tag off a shelf** — the Urovo can
|
||||
only read/write a tag physically inside the printer.
|
||||
BIN
UHFPrimeReader.dll
Normal file
BIN
UHFPrimeReader.dll
Normal file
Binary file not shown.
7
build.cmd
Normal file
7
build.cmd
Normal file
@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
REM Rebuild the daemon after editing daemon.cs. No .NET SDK required — uses the
|
||||
REM .NET Framework 4.x compiler that ships with Windows.
|
||||
cd /d "%~dp0"
|
||||
"C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe" /nologo /platform:x64 /target:exe /out:pricegod-urovo-daemon.exe /r:GTSPL_SDK.dll /r:System.Web.Extensions.dll daemon.cs
|
||||
if %errorlevel%==0 (echo BUILD OK) else (echo BUILD FAILED - see errors above)
|
||||
pause
|
||||
152
chafcheck.cs
Normal file
152
chafcheck.cs
Normal file
@ -0,0 +1,152 @@
|
||||
// chafcheck — probe the Chafon UHF reader via the native UHFPrimeReader.dll C API (P/Invoke).
|
||||
// Validates connection + read + write before wiring a Chafon /write-tag into the daemon.
|
||||
// Needs UHFPrimeReader.dll + hidapi.dll (x64) beside the exe.
|
||||
//
|
||||
// chafcheck info -> list USB-HID readers (no tag needed)
|
||||
// chafcheck read -> read the EPC of the tag in the field
|
||||
// chafcheck write <24-hex-EPC> -> write EPC to the tag in the field, then read back
|
||||
//
|
||||
// Build (x64, .NET Framework):
|
||||
// csc /platform:x64 /out:chafcheck.exe chafcheck.cs
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
class ChafCheck
|
||||
{
|
||||
const string DLL = "UHFPrimeReader.dll";
|
||||
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)] static extern int CFHid_GetUsbCount();
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)] static extern int CFHid_GetUsbInfo(ushort index, byte[] pucDeviceInfo);
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)] static extern int OpenHidConnection(ref long hComm, ushort index);
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)] static extern int OpenDevice(ref long hComm, string pcCom, int iBaudRate);
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)] static extern int CloseDevice(long hComm);
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)] static extern int GetTagUii(long hComm, ref TagInfo tag, ushort timeout);
|
||||
[DllImport(DLL, CallingConvention = CallingConvention.Cdecl)] static extern int WriteTag(long hComm, byte option, byte[] accPwd, byte memBank, ushort wordPtr, byte wordCount, byte[] writeData);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
struct TagInfo
|
||||
{
|
||||
public ushort NO;
|
||||
public short rssi;
|
||||
public byte antenna;
|
||||
public byte channel;
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] public byte[] crc;
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] public byte[] pc;
|
||||
public byte codeLen;
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 255)] public byte[] code;
|
||||
}
|
||||
|
||||
static TagInfo NewTag() { return new TagInfo { crc = new byte[2], pc = new byte[2], code = new byte[255] }; }
|
||||
|
||||
static long Connect(string com)
|
||||
{
|
||||
long h = 0;
|
||||
int cnt = CFHid_GetUsbCount();
|
||||
Console.WriteLine("CFHid_GetUsbCount = " + cnt);
|
||||
if (cnt > 0)
|
||||
{
|
||||
int r = OpenHidConnection(ref h, 0);
|
||||
Console.WriteLine("OpenHidConnection(0) = 0x" + r.ToString("X") + (r == 0 ? " (ok)" : " (FAIL)"));
|
||||
if (r == 0) return h;
|
||||
}
|
||||
// COM4/COM5 here are the Urovo's Bluetooth links — skip them. The H-102 is the CH340 port.
|
||||
string[] ports = com != null ? new[] { com } : new[] { "COM3", "COM6", "COM7", "COM8" };
|
||||
foreach (var c in ports)
|
||||
{
|
||||
long hh = 0;
|
||||
int r = OpenDevice(ref hh, c, 115200);
|
||||
Console.WriteLine("OpenDevice(" + c + ",115200) = 0x" + r.ToString("X"));
|
||||
if (r == 0) return hh;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static string Hex(byte[] b, int n) { var sb = new StringBuilder(); for (int i = 0; i < n && i < b.Length; i++) sb.Append(b[i].ToString("X2")); return sb.ToString(); }
|
||||
static byte[] FromHex(string h) { byte[] b = new byte[h.Length / 2]; for (int i = 0; i < b.Length; i++) b[i] = Convert.ToByte(h.Substring(2 * i, 2), 16); return b; }
|
||||
|
||||
static string CodeName(int c)
|
||||
{
|
||||
switch ((uint)c)
|
||||
{
|
||||
case 0x00000000: return "(OK)";
|
||||
case 0xFFFFFF08: return "(TAG no-resp — reader ANSWERS, just no tag in field ← good baud!)";
|
||||
case 0xFFFFFF12: return "(comm timeout — reader not answering at this baud)";
|
||||
case 0xFFFFFF13: return "(serial write failed)";
|
||||
case 0xFFFFFF14: return "(serial read failed)";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
string action = args.Length > 0 ? args[0] : "info";
|
||||
try
|
||||
{
|
||||
if (action == "info")
|
||||
{
|
||||
int cnt = CFHid_GetUsbCount();
|
||||
Console.WriteLine("USB-HID Chafon reader count = " + cnt);
|
||||
for (ushort i = 0; i < cnt; i++)
|
||||
{
|
||||
var buf = new byte[512];
|
||||
CFHid_GetUsbInfo(i, buf);
|
||||
Console.WriteLine(" [" + i + "] " + Encoding.ASCII.GetString(buf).Trim('\0'));
|
||||
}
|
||||
if (cnt == 0) Console.WriteLine("(none on USB-HID; reader may be on a COM port — try 'read')");
|
||||
return;
|
||||
}
|
||||
|
||||
string com = null;
|
||||
foreach (var a in args) if (System.Text.RegularExpressions.Regex.IsMatch(a, "^(?i)COM\\d+$")) com = a.ToUpper();
|
||||
|
||||
if (action == "scan")
|
||||
{
|
||||
int[] bauds = { 115200, 57600, 38400, 19200, 9600, 230400, 460800 };
|
||||
foreach (int b in bauds)
|
||||
{
|
||||
long hh = 0;
|
||||
int r = OpenDevice(ref hh, com ?? "COM3", b);
|
||||
if (r != 0) { Console.WriteLine("baud " + b + ": open failed 0x" + r.ToString("X")); continue; }
|
||||
var t = NewTag();
|
||||
int rr = GetTagUii(hh, ref t, 1200);
|
||||
Console.WriteLine("baud " + b + ": GetTagUii = 0x" + rr.ToString("X") + " " + CodeName(rr) + (rr == 0 ? (" EPC=" + Hex(t.code, 12)) : ""));
|
||||
CloseDevice(hh);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
long h = Connect(com);
|
||||
if (h == 0) { Console.WriteLine("RESULT: could not connect to the Chafon reader"); return; }
|
||||
try
|
||||
{
|
||||
if (action == "read")
|
||||
{
|
||||
var tag = NewTag();
|
||||
int r = GetTagUii(h, ref tag, 3000);
|
||||
Console.WriteLine("GetTagUii = 0x" + r.ToString("X"));
|
||||
if (r == 0) Console.WriteLine("EPC = " + Hex(tag.code, 12) + " len=" + tag.codeLen + " rssi=" + (tag.rssi / 10) + " ant=" + tag.antenna);
|
||||
else Console.WriteLine("(no tag in field / error)");
|
||||
}
|
||||
else if (action == "write")
|
||||
{
|
||||
string epc = args.Length > 1 ? args[1] : "1234567890ABCDEF12345678";
|
||||
byte[] data = FromHex(epc); // 12 bytes
|
||||
byte[] pwd = new byte[4]; // access pwd 00000000
|
||||
int r = WriteTag(h, 0, pwd, 1, 2, (byte)(data.Length / 2), data); // memBank 1 = EPC/UII, wordPtr 2, 6 words
|
||||
Console.WriteLine("WriteTag(EPC=" + epc + ") = 0x" + r.ToString("X") + (r == 0 ? " (ok)" : " (FAIL)"));
|
||||
var tag = NewTag();
|
||||
if (GetTagUii(h, ref tag, 2000) == 0) Console.WriteLine("read-back EPC = " + Hex(tag.code, 12));
|
||||
else Console.WriteLine("read-back: no tag / error");
|
||||
}
|
||||
}
|
||||
finally { CloseDevice(h); Console.WriteLine("closed."); }
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("EXCEPTION: " + ex.GetType().Name + ": " + ex.Message);
|
||||
if (ex.InnerException != null) Console.WriteLine(" inner: " + ex.InnerException.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
518
daemon.cs
Normal file
518
daemon.cs
Normal file
@ -0,0 +1,518 @@
|
||||
// pricegod-urovo-daemon — a localhost:7790 HTTP bridge that drives the Urovo D812R+
|
||||
// (RFID label printer) via the GTSPL SDK, doing PRINT + RFID ENCODE in one pass.
|
||||
//
|
||||
// This is a drop-in for the extension's existing daemon.js client (same :7790, CORS *).
|
||||
// It wraps the exact, proven sequence from gtcheck.cs (SET RIBBON OFF -> continuous
|
||||
// geometry -> writeUHF -> draw -> printlabel -> verify), so all the hard-won Urovo
|
||||
// quirks (ribbon-off for direct thermal, GAP 0 continuous mode for RFID rolls that
|
||||
// blind the optical gap sensor) are preserved.
|
||||
//
|
||||
// Build (no .NET SDK needed — uses the always-present .NET Framework compiler):
|
||||
// C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe /nologo /platform:x64 \
|
||||
// /target:exe /out:pricegod-urovo-daemon.exe \
|
||||
// /r:GTSPL_SDK.dll /r:System.Web.Extensions.dll daemon.cs
|
||||
// (GTSPL_SDK.dll, GTSPL_SDK_C.dll, zlib.net.dll must sit beside the .exe at runtime.)
|
||||
//
|
||||
// Routes (all CORS *):
|
||||
// GET /status detect printer + read status -> {ok,ready,status,statusText,printer}
|
||||
// POST /print-encode label fields + sku + releaseId -> {ok,sku,releaseId,epc,verified,status}
|
||||
// POST /write-tag alias of /print-encode (daemon.js name)
|
||||
// GET /read-tag read the tag currently at the antenna -> {ok,epc,sku,releaseId}
|
||||
// POST /calibrate RFID auto-calibration
|
||||
// POST /recover ribbon-off + one feed to clear a fault
|
||||
//
|
||||
// NOTE ON THE SKU<->EPC SCHEME (see Epc class): v1 packs the 14-digit timestamp SKU
|
||||
// (6 bytes) + release_id (4 bytes) + a 0xEC01 marker (2 bytes) into the 96-bit EPC.
|
||||
// This MUST match whatever the Mac/Chafon daemon writes so tags interoperate. If it
|
||||
// doesn't, change ONLY the Epc class — nothing else depends on the byte layout.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Web.Script.Serialization;
|
||||
using GTSPL_SDK;
|
||||
|
||||
class UrovoDaemon
|
||||
{
|
||||
const string Version = "pricegod-urovo-daemon/1.0";
|
||||
static readonly object PrinterLock = new object(); // the SDK holds one USB handle; serialize all hardware access
|
||||
static readonly JavaScriptSerializer J = new JavaScriptSerializer();
|
||||
|
||||
// ---- label / geometry defaults (overridable per-request) ----
|
||||
// Defaults = the shop's 54x24mm gapped Dymo thermal labels (extension's "large").
|
||||
// Other stocks: small 51x19, xlarge 64x34, or 65x35 continuous UHF RFID (pass w/h/gap).
|
||||
const int DEF_W = 54, DEF_H = 24, DEF_GAP = 2, DEF_DENSITY = 15; // 15 = max darkness
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
int port = 7790;
|
||||
for (int i = 0; i < args.Length - 1; i++)
|
||||
if (args[i] == "--port") int.TryParse(args[i + 1], out port);
|
||||
|
||||
var listener = new HttpListener();
|
||||
listener.Prefixes.Add("http://localhost:" + port + "/");
|
||||
listener.Prefixes.Add("http://127.0.0.1:" + port + "/");
|
||||
try { listener.Start(); }
|
||||
catch (HttpListenerException ex)
|
||||
{
|
||||
Console.WriteLine("FATAL: could not listen on :" + port + " -> " + ex.Message);
|
||||
Console.WriteLine("If this is 'Access is denied', run ONCE in an elevated prompt:");
|
||||
Console.WriteLine(" netsh http add urlacl url=http://localhost:" + port + "/ user=Everyone");
|
||||
return;
|
||||
}
|
||||
Console.WriteLine(Version + " listening on http://localhost:" + port + "/ (Ctrl+C to stop)");
|
||||
Console.WriteLine("routes: GET /status POST /print-encode POST /write-tag GET /read-tag POST /calibrate POST /recover");
|
||||
|
||||
while (true)
|
||||
{
|
||||
HttpListenerContext ctx;
|
||||
try { ctx = listener.GetContext(); }
|
||||
catch { break; }
|
||||
ThreadPool.QueueUserWorkItem(delegate { Handle(ctx); });
|
||||
}
|
||||
}
|
||||
|
||||
static void Handle(HttpListenerContext ctx)
|
||||
{
|
||||
var req = ctx.Request;
|
||||
var res = ctx.Response;
|
||||
res.AddHeader("Access-Control-Allow-Origin", "*");
|
||||
res.AddHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||
res.AddHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||
|
||||
string path = req.Url.AbsolutePath.TrimEnd('/');
|
||||
if (path.Length == 0) path = "/";
|
||||
|
||||
try
|
||||
{
|
||||
if (req.HttpMethod == "OPTIONS") { res.StatusCode = 204; res.Close(); return; }
|
||||
|
||||
string body = "";
|
||||
if (req.HttpMethod == "POST")
|
||||
using (var sr = new StreamReader(req.InputStream, req.ContentEncoding ?? Encoding.UTF8))
|
||||
body = sr.ReadToEnd();
|
||||
var input = ParseObj(body);
|
||||
|
||||
Console.WriteLine(DateTime.Now.ToString("HH:mm:ss") + " " + req.HttpMethod + " " + path);
|
||||
|
||||
Dictionary<string, object> result;
|
||||
switch (path)
|
||||
{
|
||||
case "/status": result = DoStatus(); break;
|
||||
case "/print-encode": case "/write-tag": result = DoPrintEncode(input, true); break;
|
||||
case "/print-test": result = DoPrintEncode(input, false); break;
|
||||
case "/read-tag": result = DoReadTag(); break;
|
||||
case "/read-raw": result = DoReadRaw(); break;
|
||||
case "/calibrate": result = DoCalibrate(); break;
|
||||
case "/recover": result = DoRecover(); break;
|
||||
case "/": result = Map("ok", true, "service", Version); break;
|
||||
default: res.StatusCode = 404; result = Map("ok", false, "error", "unknown route " + path); break;
|
||||
}
|
||||
WriteJson(res, result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteJson(res, Map("ok", false, "error", ex.GetType().Name + ": " + ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- routes ----------
|
||||
|
||||
static Dictionary<string, object> DoStatus()
|
||||
{
|
||||
lock (PrinterLock)
|
||||
{
|
||||
var usb = new USB();
|
||||
string target = FirstPrinter(usb);
|
||||
if (target == null) return Map("ok", false, "ready", false, "error", "no USB printer detected by SDK");
|
||||
if (usb.openports_USB(target) == 0) return Map("ok", false, "ready", false, "printer", target, "error", "openports failed");
|
||||
try
|
||||
{
|
||||
string st = (usb.printerstatus_USB() ?? "").Trim();
|
||||
return Map("ok", true, "ready", st == "00", "status", st, "statusText", StatusText(st), "printer", target);
|
||||
}
|
||||
finally { usb.closeport_USB(); }
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<string, object> DoPrintEncode(Dictionary<string, object> input, bool encode)
|
||||
{
|
||||
string sku = Str(input, "sku");
|
||||
string releaseId = Str(input, "releaseId");
|
||||
if (releaseId.Length == 0) releaseId = Str(input, "release_id");
|
||||
|
||||
string epc = "";
|
||||
if (encode)
|
||||
{
|
||||
string err;
|
||||
if (!Epc.TryEncode(sku, releaseId, out epc, out err))
|
||||
return Map("ok", false, "error", err, "sku", sku, "releaseId", releaseId);
|
||||
}
|
||||
|
||||
int w = Int(input, "w", DEF_W), h = Int(input, "h", DEF_H), gap = Int(input, "gap", DEF_GAP);
|
||||
int dir = Int(input, "dir", 1); // print direction 0/1 — flip if the label comes out rotated
|
||||
|
||||
// human-readable label fields (all optional) — mirror the extension's label design
|
||||
string artist = Str(input, "artist"), title = Str(input, "title");
|
||||
string genre = Str(input, "genre"), style = Str(input, "style");
|
||||
string info = Str(input, "info");
|
||||
string price = Str(input, "price"), condition = Str(input, "condition");
|
||||
|
||||
lock (PrinterLock)
|
||||
{
|
||||
var usb = new USB();
|
||||
string target = FirstPrinter(usb);
|
||||
if (target == null) return Map("ok", false, "error", "no USB printer detected");
|
||||
if (usb.openports_USB(target) == 0) return Map("ok", false, "error", "openports failed", "printer", target);
|
||||
try
|
||||
{
|
||||
// 1) direct-thermal (ribbon off). Wait out any transient/busy state first;
|
||||
// only feed-to-clear if it's a REAL, persistent fault (avoids wasting a label
|
||||
// on a transient RFID/print-busy code like 20/31/45).
|
||||
usb.sendcommand_USB("SET RIBBON OFF");
|
||||
Thread.Sleep(400);
|
||||
string s0 = WaitReady(usb, 3000);
|
||||
if (s0 != "00")
|
||||
{
|
||||
usb.formfeed_USB();
|
||||
Thread.Sleep(1400);
|
||||
usb.sendcommand_USB("SET RIBBON OFF");
|
||||
Thread.Sleep(400);
|
||||
WaitReady(usb, 3000);
|
||||
}
|
||||
|
||||
// 2) geometry. gap>0 = die-cut label mode via the optical gap sensor (Dymo 54x24);
|
||||
// gap=0 = continuous/fixed-length (UHF RFID rolls that blind the gap sensor).
|
||||
usb.sendcommand_USB("SET TEAR ON");
|
||||
usb.sendcommand_USB("GAP " + gap + " mm,0 mm");
|
||||
usb.sendcommand_USB("SIZE " + w + " mm," + h + " mm");
|
||||
usb.sendcommand_USB("DENSITY " + DEF_DENSITY);
|
||||
usb.sendcommand_USB("DIRECTION " + dir);
|
||||
usb.clearbuffer_USB();
|
||||
|
||||
string before = encode ? Clean(usb.readUHF_USB("H", 2, 12, "E")) : "(test)";
|
||||
|
||||
// 3) stage the RFID write (skipped in thermal-only test mode), then the visible label
|
||||
if (encode) usb.writeUHF_USB("H", 2, 12, "E", epc);
|
||||
// --- layout (dots @ 203dpi = 8/mm): two columns like the extension's design.
|
||||
// left = artist/title/genre/style/info ; right = price / condition / QR.
|
||||
int Wd = w * 8; // label width in dots
|
||||
int rightX = Wd - 96; // right column start
|
||||
// right column
|
||||
if (price.Length > 0) usb.printerfont_USB(rightX.ToString(), "8", "4", "0", "1", "1", Trunc(price, 6));
|
||||
if (condition.Length > 0) usb.printerfont_USB(rightX.ToString(), "48", "1", "0", "1", "1", Trunc(condition, 12));
|
||||
if (sku.Length > 0) usb.qrcode_USB((rightX + 6).ToString(), "66", "M", "3", "A", "0", sku);
|
||||
// left column
|
||||
int lx = 8, ty = 4, leftCh = Math.Max(8, (rightX - lx) / 8);
|
||||
if (artist.Length > 0) { usb.printerfont_USB(lx.ToString(), ty.ToString(), "2", "0", "1", "1", Trunc(artist, leftCh * 2 / 3)); ty += 24; }
|
||||
if (title.Length > 0) { usb.printerfont_USB(lx.ToString(), ty.ToString(), "2", "0", "1", "1", Trunc(title, leftCh * 2 / 3)); ty += 24; }
|
||||
if (genre.Length > 0) { usb.printerfont_USB(lx.ToString(), ty.ToString(), "1", "0", "1", "1", Trunc(genre, leftCh)); ty += 16; }
|
||||
if (style.Length > 0) { usb.printerfont_USB(lx.ToString(), ty.ToString(), "2", "0", "1", "1", Trunc(style, leftCh * 2 / 3)); ty += 24; }
|
||||
if (info.Length > 0) { usb.printerfont_USB(lx.ToString(), ty.ToString(), "1", "0", "1", "1", Trunc(info, leftCh)); ty += 16; }
|
||||
if (sku.Length > 0 || releaseId.Length > 0)
|
||||
usb.printerfont_USB(lx.ToString(), ty.ToString(), "1", "0", "1", "1", Trunc((sku + " #" + releaseId).Trim(), leftCh));
|
||||
|
||||
// 4) ribbon off LAST, then fire print + encode together
|
||||
usb.sendcommand_USB("SET RIBBON OFF");
|
||||
Thread.Sleep(500);
|
||||
usb.printlabel_USB("1", "1");
|
||||
|
||||
// 5) WAIT for the printer to finish (RFID write + feed can sit in a busy
|
||||
// state like 20/31/45 for a couple seconds). Success = it returns to 00
|
||||
// with no VOID. A read-back here would read the NEXT (blank) tag because
|
||||
// the encoded one has fed to the tear bar, so we DON'T gate success on it.
|
||||
string st = WaitReady(usb, 8000);
|
||||
string nextTag = Clean(usb.readUHF_USB("H", 2, 12, "E")); // best-effort: whatever's at antenna now
|
||||
usb.sendcommand_USB("SET RIBBON OFF"); // re-assert as final state
|
||||
|
||||
bool printed = st == "00";
|
||||
return Map(
|
||||
"ok", printed,
|
||||
"sku", sku, "releaseId", releaseId, "epc", epc,
|
||||
"epcBefore", before,
|
||||
"nextTagAtAntenna", nextTag,
|
||||
"status", st, "statusText", StatusText(st),
|
||||
"mode", encode ? "encode+print" : "thermal-test",
|
||||
"note", printed
|
||||
? (encode ? "printer returned to Ready (no VOID) — encode+print OK; verify by scanning the label's chip"
|
||||
: "thermal-only test at DENSITY " + DEF_DENSITY + " — check darkness/legibility")
|
||||
: "printer did NOT return to Ready in 8s — check for VOID / jam / tag position");
|
||||
}
|
||||
finally { usb.closeport_USB(); }
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<string, object> DoReadTag()
|
||||
{
|
||||
lock (PrinterLock)
|
||||
{
|
||||
var usb = new USB();
|
||||
string target = FirstPrinter(usb);
|
||||
if (target == null) return Map("ok", false, "error", "no USB printer detected");
|
||||
if (usb.openports_USB(target) == 0) return Map("ok", false, "error", "openports failed");
|
||||
try
|
||||
{
|
||||
string epc = Clean(usb.query_UHF_USB("H", 0, 0));
|
||||
if (epc.Length == 0 || epc.StartsWith("6C")) return Map("ok", false, "empty", true, "error", "no tag at antenna");
|
||||
string sku, releaseId;
|
||||
bool ours = Epc.TryDecode(epc, out sku, out releaseId);
|
||||
return Map("ok", true, "epc", epc, "sku", ours ? sku : null, "releaseId", ours ? releaseId : null, "recognized", ours);
|
||||
}
|
||||
finally { usb.closeport_USB(); }
|
||||
}
|
||||
}
|
||||
|
||||
// Diagnostic: dump every memory bank of the tag at the antenna, raw hex, so we can
|
||||
// reverse-engineer whatever scheme the Mac/Chafon daemon uses. Read-only.
|
||||
static Dictionary<string, object> DoReadRaw()
|
||||
{
|
||||
lock (PrinterLock)
|
||||
{
|
||||
var usb = new USB();
|
||||
string target = FirstPrinter(usb);
|
||||
if (target == null) return Map("ok", false, "error", "no USB printer detected");
|
||||
if (usb.openports_USB(target) == 0) return Map("ok", false, "error", "openports failed");
|
||||
try
|
||||
{
|
||||
var banks = new Dictionary<string, object>();
|
||||
banks["query_epc"] = Clean(usb.query_UHF_USB("H", 0, 0));
|
||||
banks["epc_block2_12"] = SafeRead(usb, "H", 2, 12, "E"); // the 96-bit EPC
|
||||
banks["epc_block0_16"] = SafeRead(usb, "H", 0, 16, "E"); // incl PC/CRC words
|
||||
banks["user_0_32"] = SafeRead(usb, "H", 0, 32, "U"); // first 32 bytes of USER
|
||||
banks["user_0_64"] = SafeRead(usb, "H", 0, 64, "U"); // first 64 bytes of USER
|
||||
banks["tid_0_12"] = SafeRead(usb, "H", 0, 12, "T"); // TID (factory id)
|
||||
return Map("ok", true, "printer", target, "banks", banks);
|
||||
}
|
||||
finally { usb.closeport_USB(); }
|
||||
}
|
||||
}
|
||||
|
||||
static string SafeRead(USB usb, string fmt, int blk, int n, string bank)
|
||||
{
|
||||
try { return Clean(usb.readUHF_USB(fmt, blk, n, bank)); }
|
||||
catch (Exception ex) { return "ERR:" + ex.Message; }
|
||||
}
|
||||
|
||||
static Dictionary<string, object> DoCalibrate()
|
||||
{
|
||||
lock (PrinterLock)
|
||||
{
|
||||
var usb = new USB();
|
||||
string target = FirstPrinter(usb);
|
||||
if (target == null) return Map("ok", false, "error", "no USB printer detected");
|
||||
if (usb.openports_USB(target) == 0) return Map("ok", false, "error", "openports failed");
|
||||
try
|
||||
{
|
||||
usb.RFIDAutoCalibration_USB();
|
||||
Thread.Sleep(1500);
|
||||
string st = (usb.printerstatus_USB() ?? "").Trim();
|
||||
return Map("ok", st == "00", "status", st, "statusText", StatusText(st));
|
||||
}
|
||||
finally { usb.closeport_USB(); }
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<string, object> DoRecover()
|
||||
{
|
||||
lock (PrinterLock)
|
||||
{
|
||||
var usb = new USB();
|
||||
string target = FirstPrinter(usb);
|
||||
if (target == null) return Map("ok", false, "error", "no USB printer detected");
|
||||
if (usb.openports_USB(target) == 0) return Map("ok", false, "error", "openports failed");
|
||||
try
|
||||
{
|
||||
usb.sendcommand_USB("SET RIBBON OFF");
|
||||
Thread.Sleep(400);
|
||||
usb.formfeed_USB();
|
||||
Thread.Sleep(1400);
|
||||
usb.sendcommand_USB("SET RIBBON OFF");
|
||||
Thread.Sleep(400);
|
||||
string st = (usb.printerstatus_USB() ?? "").Trim();
|
||||
return Map("ok", st == "00", "status", st, "statusText", StatusText(st));
|
||||
}
|
||||
finally { usb.closeport_USB(); }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
static string FirstPrinter(USB usb)
|
||||
{
|
||||
string[] names = usb.detectUSB_USB();
|
||||
if (names == null) return null;
|
||||
string last = null;
|
||||
foreach (var n in names) if (n != null) last = n; // gtcheck used the last non-null
|
||||
return last;
|
||||
}
|
||||
|
||||
static string Clean(string d)
|
||||
{
|
||||
return (d ?? "").Replace("\r", "").Replace("\n", "").Trim();
|
||||
}
|
||||
|
||||
static string Trunc(string s, int n)
|
||||
{
|
||||
s = s ?? "";
|
||||
return s.Length <= n ? s : s.Substring(0, n);
|
||||
}
|
||||
|
||||
// Poll the printer until it reports Ready ("00") or we hit the timeout. Transient
|
||||
// codes seen during RFID-encode + feed on the D812R+: 20 (printing), 31, 45 (busy).
|
||||
static string WaitReady(USB usb, int timeoutMs)
|
||||
{
|
||||
int waited = 0;
|
||||
string st = "";
|
||||
while (waited < timeoutMs)
|
||||
{
|
||||
st = (usb.printerstatus_USB() ?? "").Trim();
|
||||
if (st == "00") return st;
|
||||
Thread.Sleep(300);
|
||||
waited += 300;
|
||||
}
|
||||
return st;
|
||||
}
|
||||
|
||||
static string StatusText(string s)
|
||||
{
|
||||
switch ((s ?? "").Trim())
|
||||
{
|
||||
case "00": return "Normal / Ready";
|
||||
case "31": return "Busy (RFID/print in progress)";
|
||||
case "45": return "Busy (RFID/print in progress)";
|
||||
case "01": return "Head opened";
|
||||
case "02": return "Paper Jam";
|
||||
case "03": return "Paper jam and head opened";
|
||||
case "04": return "Out of paper";
|
||||
case "05": return "Out of paper and head opened";
|
||||
case "08": return "Out of ribbon";
|
||||
case "09": return "Out of ribbon and head opened";
|
||||
case "0A": return "Out of ribbon and paper jam";
|
||||
case "0C": return "Out of ribbon and out of paper";
|
||||
case "10": return "Pause";
|
||||
case "20": return "Printing";
|
||||
case "80": return "Other error";
|
||||
default: return "(unmapped: " + s + ")";
|
||||
}
|
||||
}
|
||||
|
||||
static Dictionary<string, object> ParseObj(string body)
|
||||
{
|
||||
if (string.IsNullOrEmpty(body)) return new Dictionary<string, object>();
|
||||
try
|
||||
{
|
||||
var o = J.DeserializeObject(body) as Dictionary<string, object>;
|
||||
return o ?? new Dictionary<string, object>();
|
||||
}
|
||||
catch { return new Dictionary<string, object>(); }
|
||||
}
|
||||
|
||||
static string Str(Dictionary<string, object> d, string key)
|
||||
{
|
||||
object v;
|
||||
if (d != null && d.TryGetValue(key, out v) && v != null) return Convert.ToString(v, CultureInfo.InvariantCulture);
|
||||
return "";
|
||||
}
|
||||
|
||||
static int Int(Dictionary<string, object> d, string key, int def)
|
||||
{
|
||||
object v; int n;
|
||||
if (d != null && d.TryGetValue(key, out v) && v != null && int.TryParse(Convert.ToString(v, CultureInfo.InvariantCulture), out n)) return n;
|
||||
return def;
|
||||
}
|
||||
|
||||
static Dictionary<string, object> Map(params object[] kv)
|
||||
{
|
||||
var d = new Dictionary<string, object>();
|
||||
for (int i = 0; i + 1 < kv.Length; i += 2) d[Convert.ToString(kv[i])] = kv[i + 1];
|
||||
return d;
|
||||
}
|
||||
|
||||
static void WriteJson(HttpListenerResponse res, Dictionary<string, object> obj)
|
||||
{
|
||||
byte[] buf = Encoding.UTF8.GetBytes(J.Serialize(obj));
|
||||
res.ContentType = "application/json";
|
||||
res.ContentLength64 = buf.Length;
|
||||
res.OutputStream.Write(buf, 0, buf.Length);
|
||||
res.OutputStream.Close();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SKU <-> EPC packing (v1). 96-bit EPC = 12 bytes:
|
||||
// [0..5] 6 bytes = 14-digit timestamp SKU as a big-endian integer (fits 48 bits)
|
||||
// [6..9] 4 bytes = release_id as a big-endian uint32
|
||||
// [10..11]2 bytes = 0xEC01 marker (identifies "our" tags on read-back)
|
||||
// Change ONLY this class if the Mac/Chafon scheme differs — nothing else cares.
|
||||
// ============================================================================
|
||||
static class Epc
|
||||
{
|
||||
public static bool TryEncode(string sku14, string releaseId, out string hex, out string err)
|
||||
{
|
||||
hex = null; err = null;
|
||||
sku14 = (sku14 ?? "").Trim();
|
||||
releaseId = (releaseId ?? "").Trim();
|
||||
if (sku14.Length != 14 || !AllDigits(sku14)) { err = "sku must be 14 digits"; return false; }
|
||||
ulong skuNum;
|
||||
if (!ulong.TryParse(sku14, out skuNum)) { err = "sku not numeric"; return false; }
|
||||
uint rel;
|
||||
if (!uint.TryParse(releaseId.Length == 0 ? "0" : releaseId, out rel)) { err = "releaseId not a uint32"; return false; }
|
||||
|
||||
byte[] b = new byte[12];
|
||||
ulong s = skuNum;
|
||||
for (int i = 5; i >= 0; i--) { b[i] = (byte)(s & 0xFF); s >>= 8; }
|
||||
b[6] = (byte)(rel >> 24); b[7] = (byte)(rel >> 16); b[8] = (byte)(rel >> 8); b[9] = (byte)rel;
|
||||
b[10] = 0xEC; b[11] = 0x01;
|
||||
hex = ToHex(b);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryDecode(string hex, out string sku14, out string releaseId)
|
||||
{
|
||||
sku14 = null; releaseId = null;
|
||||
hex = (hex ?? "").Trim();
|
||||
if (hex.Length != 24) return false;
|
||||
byte[] b = FromHex(hex);
|
||||
if (b == null) return false;
|
||||
if (!(b[10] == 0xEC && b[11] == 0x01)) return false; // not our scheme
|
||||
ulong s = 0; for (int i = 0; i < 6; i++) s = (s << 8) | b[i];
|
||||
uint rel = ((uint)b[6] << 24) | ((uint)b[7] << 16) | ((uint)b[8] << 8) | b[9];
|
||||
sku14 = s.ToString(CultureInfo.InvariantCulture).PadLeft(14, '0');
|
||||
releaseId = rel.ToString(CultureInfo.InvariantCulture);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool AllDigits(string s) { foreach (char c in s) if (c < '0' || c > '9') return false; return s.Length > 0; }
|
||||
|
||||
static string ToHex(byte[] b)
|
||||
{
|
||||
var sb = new StringBuilder(b.Length * 2);
|
||||
foreach (byte x in b) sb.Append(x.ToString("X2"));
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
static byte[] FromHex(string h)
|
||||
{
|
||||
if ((h.Length & 1) != 0) return null;
|
||||
byte[] b = new byte[h.Length / 2];
|
||||
for (int i = 0; i < b.Length; i++)
|
||||
{
|
||||
int hi = HexVal(h[2 * i]), lo = HexVal(h[2 * i + 1]);
|
||||
if (hi < 0 || lo < 0) return null;
|
||||
b[i] = (byte)((hi << 4) | lo);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
static int HexVal(char c)
|
||||
{
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
BIN
hidapi.dll
Normal file
BIN
hidapi.dll
Normal file
Binary file not shown.
9
start-daemon.cmd
Normal file
9
start-daemon.cmd
Normal file
@ -0,0 +1,9 @@
|
||||
@echo off
|
||||
REM Double-click to start the PriceGod Urovo daemon on http://localhost:7790/
|
||||
REM Leave this window open while you print/encode. Close it (or Ctrl+C) to stop.
|
||||
cd /d "%~dp0"
|
||||
title pricegod-urovo-daemon (:7790)
|
||||
pricegod-urovo-daemon.exe %*
|
||||
echo.
|
||||
echo daemon stopped. press any key to close.
|
||||
pause >nul
|
||||
BIN
zlib.net.dll
Normal file
BIN
zlib.net.dll
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user