- 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>
519 lines
23 KiB
C#
519 lines
23 KiB
C#
// 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;
|
|
}
|
|
}
|