feat(pos): CLI-диагностика оборудования кассы --probe-hw и тест-печать --test-print
- ProbeRunner: COM-порты (имя/VID/PID/описание через WMI), open→close (занят/свободен без удержания), опрос весов Масса-К+CAS 9600 (таймаут 1.5с), принтеры Windows (имя/порт/драйвер, БЕЗ печати), HID-клавиатуры (возможные сканеры). --port COMx — один порт; занятый не трогаем. - --test-print "<принтер>": отдельный флаг, ESC/POS тест-чек «ТЕСТ ALL MARKET» через spooler (winspool RAW). Без флага ничего не печатается. - WinExe → AttachConsole(parent) для видимого вывода; --out пишет чистый UTF-8 JSON. - System.Management (WMI, только Windows, под guard'ами). Проверено на Win11 VM: выход <10с, COM1/принтер/HID определяются, test-print на несуществующий принтер — мягкая ошибка (код 4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8be44055a2
commit
574de0dafb
|
|
@ -71,6 +71,7 @@
|
|||
<PackageVersion Include="Avalonia.Headless" Version="11.1.0" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.11" />
|
||||
<PackageVersion Include="System.IO.Ports" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Management" Version="8.0.0" />
|
||||
<PackageVersion Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
|
||||
|
||||
<!-- Testing -->
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
# Перенос магазина МойСклад → ALL MARKET (с историей) — прогресс
|
||||
|
||||
PROBE BUILD: ~/pos-release/allmarket-pos-win-x64-20261009-1845-probe.zip — касса `foodmarket.Pos.exe --probe-hw [--out <файл>] [--port COMx]` (COM/весы Масса-К·CAS/принтеры/HID, без печати) + `--test-print "<принтер>"` (ESC/POS тест-чек, отдельно); проверено на Win11 VM, выход < 10 с, ничего не трогает.
|
||||
|
||||
Решение владельца 09.10: перенести ВСЁ из МойСклада с историей, перейти на ALL MARKET.
|
||||
Фискализации нет и в МойСкладе (не блокер), маркировка не используется.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Avalonia;
|
||||
using foodmarket.Pos.Tooling;
|
||||
|
||||
|
|
@ -5,10 +6,29 @@ namespace foodmarket.Pos;
|
|||
|
||||
internal static class Program
|
||||
{
|
||||
[DllImport("kernel32.dll")] private static extern bool AttachConsole(int pid);
|
||||
private const int ATTACH_PARENT_PROCESS = -1;
|
||||
|
||||
/// <summary>POS собран как WinExe (GUI-subsystem) — у него нет консоли, поэтому для
|
||||
/// CLI-режимов на Windows подключаемся к консоли родителя (cmd/PowerShell), иначе вывод
|
||||
/// Console.* уходит в никуда. На Linux WinExe игнорируется — консоль есть по умолчанию.</summary>
|
||||
private static void EnsureConsole()
|
||||
{
|
||||
// Не переопределяем Console.OutputEncoding: родительская консоль (обычно cp866 на
|
||||
// RU-Windows) сама отрисует кириллицу; принудительный UTF-8 даёт кракозябры в cp866.
|
||||
// Чистый UTF-8 артефакт — файл --out (JSON пишется UTF-8 независимо от консоли).
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
try { AttachConsole(ATTACH_PARENT_PROCESS); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
PosFormat.Apply(); // единый формат чисел (KZ/RU: «3 422 ₸», «1,235 кг»)
|
||||
// CLI-режимы оборудования — подключить консоль и UTF-8 до любого вывода.
|
||||
if (args.Contains("--probe-hw") || args.Contains("--test-print")) EnsureConsole();
|
||||
// CLI-режимы (без интерактивного окна):
|
||||
// --smoke : прогон сценария кассы через логику (без UI), печать результатов.
|
||||
// --screenshots : headless-рендер экранов в docs/pos-screenshots (1920×1080 + 1366×768).
|
||||
|
|
@ -19,6 +39,15 @@ public static int Main(string[] args)
|
|||
if (args.Contains("--density")) return ScreenshotRunner.RunDensity(args);
|
||||
if (args.Contains("--uitest")) return UiTestRunner.Run(args);
|
||||
if (args.Contains("--bench")) return BenchRunner.Run(args);
|
||||
// --probe-hw : диагностика оборудования (COM/весы/принтеры/HID), без окна, без печати.
|
||||
// --test-print N : отдельный флаг — короткий тестовый чек ESC/POS на принтер Windows.
|
||||
if (args.Contains("--probe-hw")) return ProbeRunner.Run(args);
|
||||
if (args.Contains("--test-print"))
|
||||
{
|
||||
var i = Array.IndexOf(args, "--test-print");
|
||||
var name = (i >= 0 && i + 1 < args.Length) ? args[i + 1] : "";
|
||||
return ProbeRunner.TestPrint(name);
|
||||
}
|
||||
|
||||
// --demo : показать реальное окно с примерами данных (для скриншота на Windows).
|
||||
if (args.Contains("--demo") || args.Contains("--demo-pay")) App.DemoMode = true;
|
||||
|
|
|
|||
365
src/food-market.pos/Tooling/ProbeRunner.cs
Normal file
365
src/food-market.pos/Tooling/ProbeRunner.cs
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
using System.Diagnostics;
|
||||
using System.IO.Ports;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using foodmarket.Pos.Hardware;
|
||||
|
||||
namespace foodmarket.Pos.Tooling;
|
||||
|
||||
/// <summary>Диагностика оборудования кассы БЕЗ интерактивного окна (для проверки
|
||||
/// сисадмином на ноутбуке кассы, где параллельно открыта «Касса МойСклад»).
|
||||
///
|
||||
/// --probe-hw [--out файл] — отчёт: COM-порты (имя, VID/PID, описание, свободен/занят),
|
||||
/// попытка опроса весов Масса-К и CAS на свободных портах (таймаут 1–2 с), принтеры
|
||||
/// Windows (имя/порт/драйвер, БЕЗ печати), HID-клавиатуры (возможные сканеры).
|
||||
/// --probe-hw --port COM22 — опрос конкретного порта; занятый порт НЕ трогаем.
|
||||
/// --test-print "Имя принтера" — ОТДЕЛЬНО: короткий тестовый чек ESC/POS через очередь Windows.
|
||||
///
|
||||
/// Ничего не удерживает (порт открыл→закрыл), ничего не печатает без --test-print,
|
||||
/// гарантированный выход за < 10 с (общий бюджет + короткие таймауты).</summary>
|
||||
public static class ProbeRunner
|
||||
{
|
||||
private static readonly TimeSpan Budget = TimeSpan.FromSeconds(9);
|
||||
|
||||
public static int Run(string[] args)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
var only = GetArg(args, "--port");
|
||||
var outFile = GetArg(args, "--out");
|
||||
|
||||
var report = new ProbeReport
|
||||
{
|
||||
GeneratedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
|
||||
Os = RuntimeInformation.OSDescription,
|
||||
IsWindows = OperatingSystem.IsWindows(),
|
||||
};
|
||||
|
||||
// 1. COM-порты (имя/VID/PID/описание из WMI + базовый список System.IO.Ports)
|
||||
var wmiPorts = OperatingSystem.IsWindows() ? QueryComPortsWmi() : new();
|
||||
var portNames = SerialPort.GetPortNames().Distinct().ToList();
|
||||
foreach (var w in wmiPorts) if (!portNames.Contains(w.Port, StringComparer.OrdinalIgnoreCase)) portNames.Add(w.Port);
|
||||
portNames = portNames.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(p => NaturalComOrder(p)).ToList();
|
||||
if (only is not null)
|
||||
portNames = portNames.Where(p => string.Equals(p, only, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
|
||||
foreach (var port in portNames)
|
||||
{
|
||||
var wi = wmiPorts.FirstOrDefault(x => string.Equals(x.Port, port, StringComparison.OrdinalIgnoreCase));
|
||||
var p = new ComPortInfo
|
||||
{
|
||||
Port = port,
|
||||
Description = wi?.Description,
|
||||
Vid = wi?.Vid,
|
||||
Pid = wi?.Pid,
|
||||
Manufacturer = wi?.Manufacturer,
|
||||
};
|
||||
// открыть→закрыть, без удержания
|
||||
var (busy, openErr) = TryOpenClose(port);
|
||||
p.Busy = busy;
|
||||
p.OpenError = openErr;
|
||||
// опрос весов только на свободном порту и пока есть бюджет времени
|
||||
if (!busy && sw.Elapsed < Budget - TimeSpan.FromSeconds(3))
|
||||
p.Scale = ProbeScale(port, sw);
|
||||
else if (busy)
|
||||
p.Scale = "порт занят другим приложением — не опрашивался";
|
||||
else
|
||||
p.Scale = "пропущен (лимит времени)";
|
||||
report.Ports.Add(p);
|
||||
}
|
||||
|
||||
// 2. Принтеры Windows (имя/порт/драйвер) — БЕЗ печати
|
||||
if (OperatingSystem.IsWindows() && only is null)
|
||||
report.Printers = QueryPrintersWmi();
|
||||
|
||||
// 3. HID-клавиатуры (сканер ШК обычно эмулирует клавиатуру — детект без скана невозможен)
|
||||
if (OperatingSystem.IsWindows() && only is null)
|
||||
report.Keyboards = QueryKeyboardsWmi();
|
||||
|
||||
report.ElapsedMs = (int)sw.ElapsedMilliseconds;
|
||||
report.Note = OperatingSystem.IsWindows()
|
||||
? "Детект сканера без фактического скана невозможен — перечислены HID-клавиатуры."
|
||||
: "Не Windows: WMI недоступен, перечислены только COM-порты (System.IO.Ports).";
|
||||
|
||||
// Вывод: текст в stdout; JSON в --out (если задан)
|
||||
Console.WriteLine(RenderText(report));
|
||||
if (outFile is not null)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(report, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, // кириллица как есть
|
||||
});
|
||||
File.WriteAllText(outFile, json);
|
||||
Console.WriteLine($"\nJSON сохранён: {outFile}");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ───────────────────────── COM ─────────────────────────
|
||||
private static (bool busy, string? err) TryOpenClose(string port)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var sp = new SerialPort(port) { ReadTimeout = 300, WriteTimeout = 300 };
|
||||
sp.Open();
|
||||
sp.Close();
|
||||
return (false, null);
|
||||
}
|
||||
catch (UnauthorizedAccessException) { return (true, "занят другим приложением (Access denied)"); }
|
||||
catch (Exception ex) { return (false, ex.GetType().Name + ": " + ex.Message); }
|
||||
}
|
||||
|
||||
private static string ProbeScale(string port, Stopwatch sw)
|
||||
{
|
||||
foreach (var proto in new[] { ComScale.Proto.MassaK, ComScale.Proto.Cas })
|
||||
{
|
||||
if (sw.Elapsed > Budget - TimeSpan.FromSeconds(1)) return "не опрошены (лимит времени)";
|
||||
try
|
||||
{
|
||||
var scale = new ComScale(port, 9600, proto);
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(1500));
|
||||
var task = scale.ReadWeightKgAsync(cts.Token);
|
||||
if (task.Wait(1800) && task.IsCompletedSuccessfully)
|
||||
return $"весы найдены ({proto}, 9600): вес = {task.Result:0.###} кг";
|
||||
}
|
||||
catch (AggregateException ae) when (ae.InnerException is UnauthorizedAccessException)
|
||||
{ return "порт занят другим приложением"; }
|
||||
catch { /* нет ответа на этом протоколе — пробуем следующий */ }
|
||||
}
|
||||
return "весы не ответили (Масса-К/CAS, 9600, 1.5 с)";
|
||||
}
|
||||
|
||||
private static int NaturalComOrder(string p)
|
||||
=> int.TryParse(Regex.Match(p, @"\d+").Value, out var n) ? n : 9999;
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static List<WmiPort> QueryComPortsWmi()
|
||||
{
|
||||
var list = new List<WmiPort>();
|
||||
try
|
||||
{
|
||||
using var s = new System.Management.ManagementObjectSearcher(
|
||||
"SELECT Name, PNPDeviceID, Description, Manufacturer FROM Win32_PnPEntity WHERE Name LIKE '%(COM%'");
|
||||
foreach (var o in s.Get())
|
||||
{
|
||||
var name = o["Name"]?.ToString() ?? "";
|
||||
var m = Regex.Match(name, @"\((COM\d+)\)");
|
||||
if (!m.Success) continue;
|
||||
var pnp = o["PNPDeviceID"]?.ToString() ?? "";
|
||||
list.Add(new WmiPort
|
||||
{
|
||||
Port = m.Groups[1].Value,
|
||||
Description = name,
|
||||
Manufacturer = o["Manufacturer"]?.ToString(),
|
||||
Vid = Regex.Match(pnp, @"VID_([0-9A-Fa-f]{4})").Groups[1].Value is { Length: 4 } vid ? vid : null,
|
||||
Pid = Regex.Match(pnp, @"PID_([0-9A-Fa-f]{4})").Groups[1].Value is { Length: 4 } pid ? pid : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
catch { /* WMI недоступен — вернём пусто, порты всё равно возьмём из GetPortNames */ }
|
||||
return list;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static List<PrinterInfo> QueryPrintersWmi()
|
||||
{
|
||||
var list = new List<PrinterInfo>();
|
||||
try
|
||||
{
|
||||
using var s = new System.Management.ManagementObjectSearcher(
|
||||
"SELECT Name, PortName, DriverName, Default, WorkOffline FROM Win32_Printer");
|
||||
foreach (var o in s.Get())
|
||||
list.Add(new PrinterInfo
|
||||
{
|
||||
Name = o["Name"]?.ToString() ?? "",
|
||||
Port = o["PortName"]?.ToString(),
|
||||
Driver = o["DriverName"]?.ToString(),
|
||||
IsDefault = o["Default"] as bool? ?? false,
|
||||
Offline = o["WorkOffline"] as bool? ?? false,
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
return list;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static List<string> QueryKeyboardsWmi()
|
||||
{
|
||||
var list = new List<string>();
|
||||
try
|
||||
{
|
||||
using var s = new System.Management.ManagementObjectSearcher(
|
||||
"SELECT Name, Description, DeviceID FROM Win32_Keyboard");
|
||||
foreach (var o in s.Get())
|
||||
{
|
||||
var desc = o["Description"]?.ToString() ?? o["Name"]?.ToString() ?? "клавиатура";
|
||||
var id = o["DeviceID"]?.ToString();
|
||||
list.Add(string.IsNullOrEmpty(id) ? desc : $"{desc} [{id}]");
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return list;
|
||||
}
|
||||
|
||||
// ───────────────────────── тестовая печать ─────────────────────────
|
||||
public static int TestPrint(string printerName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(printerName))
|
||||
{
|
||||
Console.Error.WriteLine("Укажите имя принтера: --test-print \"Имя принтера\"");
|
||||
return 2;
|
||||
}
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
Console.Error.WriteLine("Тестовая печать через очередь Windows доступна только на Windows.");
|
||||
return 3;
|
||||
}
|
||||
var doc = new ReceiptDoc { Title = "ТЕСТ ALL MARKET", WidthChars = 48 };
|
||||
doc.Add("Проверка принтера кассы", center: true);
|
||||
doc.Add(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), center: true);
|
||||
doc.Separator();
|
||||
doc.Add("Если этот чек напечатан —", center: true);
|
||||
doc.Add("принтер и очередь Windows", center: true);
|
||||
doc.Add("работают корректно.", center: true);
|
||||
doc.Separator();
|
||||
doc.Add("ALL MARKET · касса", center: true);
|
||||
var bytes = EscPos.Build(doc);
|
||||
try
|
||||
{
|
||||
if (!RawPrinter.Send(printerName, bytes, "ТЕСТ ALL MARKET", out var err))
|
||||
{
|
||||
Console.Error.WriteLine($"Не удалось напечатать: {err}");
|
||||
return 4;
|
||||
}
|
||||
Console.WriteLine($"Тестовый чек отправлен на принтер «{printerName}» (RAW ESC/POS, 80 мм).");
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex) { Console.Error.WriteLine("Ошибка печати: " + ex.Message); return 4; }
|
||||
}
|
||||
|
||||
// ───────────────────────── вывод ─────────────────────────
|
||||
private static string RenderText(ProbeReport r)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("=== ПРОВЕРКА ОБОРУДОВАНИЯ КАССЫ (ALL MARKET) ===");
|
||||
sb.AppendLine($"Время: {r.GeneratedAt} ОС: {r.Os} (выполнено за {r.ElapsedMs} мс)");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"COM-порты ({r.Ports.Count}):");
|
||||
if (r.Ports.Count == 0) sb.AppendLine(" (не найдено)");
|
||||
foreach (var p in r.Ports)
|
||||
{
|
||||
var vidpid = (p.Vid is not null || p.Pid is not null) ? $" VID_{p.Vid}/PID_{p.Pid}" : "";
|
||||
sb.AppendLine($" {p.Port}: {(p.Busy ? "ЗАНЯТ" : "свободен")}{vidpid}");
|
||||
if (!string.IsNullOrEmpty(p.Description)) sb.AppendLine($" {p.Description}");
|
||||
if (p.Scale is not null) sb.AppendLine($" весы: {p.Scale}");
|
||||
if (p.OpenError is not null) sb.AppendLine($" порт: {p.OpenError}");
|
||||
}
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"Принтеры Windows ({r.Printers.Count}) — печать НЕ выполнялась:");
|
||||
if (r.Printers.Count == 0) sb.AppendLine(" (не найдено / не Windows)");
|
||||
foreach (var pr in r.Printers)
|
||||
sb.AppendLine($" {pr.Name}{(pr.IsDefault ? " [по умолчанию]" : "")} — порт {pr.Port}, драйвер {pr.Driver}{(pr.Offline ? ", OFFLINE" : "")}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"HID-клавиатуры ({r.Keyboards.Count}) — сканер ШК обычно в этом списке:");
|
||||
if (r.Keyboards.Count == 0) sb.AppendLine(" (не найдено / не Windows)");
|
||||
foreach (var k in r.Keyboards) sb.AppendLine($" {k}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(r.Note);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string? GetArg(string[] args, string key)
|
||||
{
|
||||
var i = Array.FindIndex(args, a => string.Equals(a, key, StringComparison.OrdinalIgnoreCase));
|
||||
return (i >= 0 && i + 1 < args.Length && !args[i + 1].StartsWith("--")) ? args[i + 1] : null;
|
||||
}
|
||||
|
||||
// ── модели отчёта ──
|
||||
private sealed class ProbeReport
|
||||
{
|
||||
public string GeneratedAt { get; set; } = "";
|
||||
public string Os { get; set; } = "";
|
||||
public bool IsWindows { get; set; }
|
||||
public int ElapsedMs { get; set; }
|
||||
public string Note { get; set; } = "";
|
||||
public List<ComPortInfo> Ports { get; set; } = new();
|
||||
public List<PrinterInfo> Printers { get; set; } = new();
|
||||
public List<string> Keyboards { get; set; } = new();
|
||||
}
|
||||
private sealed class ComPortInfo
|
||||
{
|
||||
public string Port { get; set; } = "";
|
||||
public bool Busy { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public string? Vid { get; set; }
|
||||
public string? Pid { get; set; }
|
||||
public string? Manufacturer { get; set; }
|
||||
public string? Scale { get; set; }
|
||||
public string? OpenError { get; set; }
|
||||
}
|
||||
private sealed class PrinterInfo
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string? Port { get; set; }
|
||||
public string? Driver { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
public bool Offline { get; set; }
|
||||
}
|
||||
private sealed class WmiPort
|
||||
{
|
||||
public string Port = "";
|
||||
public string? Description;
|
||||
public string? Vid;
|
||||
public string? Pid;
|
||||
public string? Manufacturer;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>RAW-печать байтов ESC/POS на именованный принтер Windows через spooler
|
||||
/// (winspool.drv). Используется только из --test-print под Windows.</summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
internal static class RawPrinter
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct DOCINFO { [MarshalAs(UnmanagedType.LPWStr)] public string pDocName; [MarshalAs(UnmanagedType.LPWStr)] public string? pOutputFile; [MarshalAs(UnmanagedType.LPWStr)] public string pDataType; }
|
||||
|
||||
[DllImport("winspool.drv", EntryPoint = "OpenPrinterW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool OpenPrinter(string src, out IntPtr h, IntPtr pd);
|
||||
[DllImport("winspool.drv", EntryPoint = "ClosePrinter", SetLastError = true)]
|
||||
private static extern bool ClosePrinter(IntPtr h);
|
||||
[DllImport("winspool.drv", EntryPoint = "StartDocPrinterW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern bool StartDocPrinter(IntPtr h, int level, ref DOCINFO di);
|
||||
[DllImport("winspool.drv", EntryPoint = "EndDocPrinter", SetLastError = true)]
|
||||
private static extern bool EndDocPrinter(IntPtr h);
|
||||
[DllImport("winspool.drv", EntryPoint = "StartPagePrinter", SetLastError = true)]
|
||||
private static extern bool StartPagePrinter(IntPtr h);
|
||||
[DllImport("winspool.drv", EntryPoint = "EndPagePrinter", SetLastError = true)]
|
||||
private static extern bool EndPagePrinter(IntPtr h);
|
||||
[DllImport("winspool.drv", EntryPoint = "WritePrinter", SetLastError = true)]
|
||||
private static extern bool WritePrinter(IntPtr h, IntPtr buf, int count, out int written);
|
||||
|
||||
public static bool Send(string printer, byte[] bytes, string docName, out string? error)
|
||||
{
|
||||
error = null;
|
||||
if (!OpenPrinter(printer, out var h, IntPtr.Zero))
|
||||
{ error = $"OpenPrinter: Win32 {Marshal.GetLastWin32Error()} (принтер не найден?)"; return false; }
|
||||
var di = new DOCINFO { pDocName = docName, pDataType = "RAW" };
|
||||
var ptr = Marshal.AllocCoTaskMem(bytes.Length);
|
||||
try
|
||||
{
|
||||
Marshal.Copy(bytes, 0, ptr, bytes.Length);
|
||||
if (!StartDocPrinter(h, 1, ref di)) { error = $"StartDocPrinter: Win32 {Marshal.GetLastWin32Error()}"; return false; }
|
||||
if (!StartPagePrinter(h)) { error = $"StartPagePrinter: Win32 {Marshal.GetLastWin32Error()}"; EndDocPrinter(h); return false; }
|
||||
var ok = WritePrinter(h, ptr, bytes.Length, out var written);
|
||||
EndPagePrinter(h);
|
||||
EndDocPrinter(h);
|
||||
if (!ok || written != bytes.Length) { error = $"WritePrinter: записано {written}/{bytes.Length}"; return false; }
|
||||
return true;
|
||||
}
|
||||
finally { Marshal.FreeCoTaskMem(ptr); ClosePrinter(h); }
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,9 @@
|
|||
<PackageReference Include="Refit.HttpClientFactory" />
|
||||
<PackageReference Include="Polly" />
|
||||
<PackageReference Include="System.IO.Ports" />
|
||||
<!-- WMI для детекта оборудования (COM VID/PID, принтеры, HID-клавиатуры) — только Windows,
|
||||
вызовы в ProbeRunner под OperatingSystem.IsWindows(). -->
|
||||
<PackageReference Include="System.Management" />
|
||||
<PackageReference Include="System.Security.Cryptography.ProtectedData" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
Loading…
Reference in a new issue