add POS895 printer adapter with LF feed and ESC * logo
Claude-Session: https://claude.ai/code/session_01E6N3P4nSZLtp4BC2LXVref
This commit is contained in:
104
Inspectron.Epson.Tests/POS895Tests.cs
Normal file
104
Inspectron.Epson.Tests/POS895Tests.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using Inspectron.Epson.PrintServer.Printers;
|
||||
using Inspectron.Epson.PrintServer.Printers.Utils;
|
||||
|
||||
namespace Inspectron.Epson.Tests;
|
||||
|
||||
public class POS895Tests
|
||||
{
|
||||
private static POS895 Make(out RecordingEpsonPrinter fake)
|
||||
{
|
||||
fake = new RecordingEpsonPrinter();
|
||||
return new POS895(fake) { SkipDelays = true };
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PrintAsync_never_uses_ESC_d_to_feed()
|
||||
{
|
||||
// This firmware ignores ESC d. Feeding with it would cut the last line.
|
||||
var printer = Make(out var fake);
|
||||
|
||||
await printer.PrintAsync(new List<PrintCommand>
|
||||
{
|
||||
new PrintCommand("hello")
|
||||
});
|
||||
|
||||
Assert.Empty(fake.FeedLineCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PrintAsync_feeds_with_literal_line_feeds_before_finishing()
|
||||
{
|
||||
var printer = Make(out var fake);
|
||||
|
||||
await printer.PrintAsync(new List<PrintCommand>
|
||||
{
|
||||
new PrintCommand("hello")
|
||||
});
|
||||
|
||||
Assert.Equal(10, fake.LineFeedBytes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PrintAsync_writes_the_command_text()
|
||||
{
|
||||
var printer = Make(out var fake);
|
||||
|
||||
await printer.PrintAsync(new List<PrintCommand>
|
||||
{
|
||||
new PrintCommand("hello")
|
||||
});
|
||||
|
||||
Assert.Contains(fake.Text, t => t.Contains("hello"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PrintAsync_handles_an_IsCut_command_with_line_feeds_then_a_cut()
|
||||
{
|
||||
var printer = Make(out var fake);
|
||||
|
||||
await printer.PrintAsync(new List<PrintCommand>
|
||||
{
|
||||
new PrintCommand("a"),
|
||||
new PrintCommand("") { IsCut = true }
|
||||
});
|
||||
|
||||
Assert.Empty(fake.FeedLineCalls);
|
||||
Assert.Equal(1, fake.CutCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PrintImageAsync_uses_bit_image_mode_at_576_dots()
|
||||
{
|
||||
// GS ( L prints nothing on this firmware, so LoadImageAsync must not be used.
|
||||
var printer = Make(out var fake);
|
||||
|
||||
await printer.PrintImageAsync("logo.png");
|
||||
|
||||
Assert.Empty(fake.RasterImages);
|
||||
var image = Assert.Single(fake.BitModeImages);
|
||||
Assert.Equal("logo.png", image.Path);
|
||||
Assert.Equal(EpsonCommands.BitImageMode.DoubleDensity24Dot, image.Mode);
|
||||
Assert.Equal(576, image.MaxWidth);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PrintImageAsync_does_not_use_absolute_positioning()
|
||||
{
|
||||
// ESC $ is unverified on this firmware; the logo spans the full width.
|
||||
var printer = Make(out var fake);
|
||||
|
||||
await printer.PrintImageAsync("logo.png");
|
||||
|
||||
Assert.Empty(fake.AbsolutePositions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cut_cuts_once()
|
||||
{
|
||||
var printer = Make(out var fake);
|
||||
|
||||
await printer.Cut();
|
||||
|
||||
Assert.Equal(1, fake.CutCount);
|
||||
}
|
||||
}
|
||||
96
Inspectron.Epson.Tests/RecordingEpsonPrinter.cs
Normal file
96
Inspectron.Epson.Tests/RecordingEpsonPrinter.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using Inspectron.Epson;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace Inspectron.Epson.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Records what an IPrinter adapter sends. Only the calls the POS895 adapter
|
||||
/// makes are recorded; the rest satisfy the interface and do nothing.
|
||||
/// </summary>
|
||||
public sealed class RecordingEpsonPrinter : IEpsonPrinter
|
||||
{
|
||||
public List<byte[]> RawCommands { get; } = new();
|
||||
public List<int> FeedLineCalls { get; } = new();
|
||||
public List<string> Text { get; } = new();
|
||||
public List<(string Path, EpsonCommands.BitImageMode Mode, int MaxWidth)> BitModeImages { get; } = new();
|
||||
public List<(string Path, int MaxWidth)> RasterImages { get; } = new();
|
||||
public List<int> AbsolutePositions { get; } = new();
|
||||
public int CutCount { get; private set; }
|
||||
|
||||
/// <summary>Total LF bytes across every raw command sent.</summary>
|
||||
public int LineFeedBytes => RawCommands.Sum(c => c.Count(b => b == 0x0A));
|
||||
|
||||
public bool IsConnected => true;
|
||||
|
||||
public Task SendRawCommandAsync(byte[] command)
|
||||
{
|
||||
RawCommands.Add(command);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task FeedLinesAsync(int lines)
|
||||
{
|
||||
FeedLineCalls.Add(lines);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task PrintTextAsync(string text)
|
||||
{
|
||||
Text.Add(text);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task PrintImageBitModeAsync(string imagePath, EpsonCommands.BitImageMode mode = EpsonCommands.BitImageMode.SingleDensity8Dot, int maxWidth = 10)
|
||||
{
|
||||
BitModeImages.Add((imagePath, mode, maxWidth));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task LoadImageAsync(string imagePath, int maxWidth = 384)
|
||||
{
|
||||
RasterImages.Add((imagePath, maxWidth));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SetAbsolutePrintPosition(int x)
|
||||
{
|
||||
AbsolutePositions.Add(x);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task CutAsync(bool fullCut = true)
|
||||
{
|
||||
CutCount++;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// Remaining interface members: no-ops.
|
||||
public Task ConnectAsync(string ip, int port = 9100, int timeoutSeconds = 10) => Task.CompletedTask;
|
||||
public void Disconnect() { }
|
||||
// 0x16 / 0x12 are what the POS895 actually returns when idle and ready.
|
||||
public Task<PrinterStatus> GetPrinterStatusAsync() => Task.FromResult(PrinterStatus.Parse(0x16));
|
||||
public Task<OfflineStatus> GetOfflineStatusAsync() => Task.FromResult(OfflineStatus.Parse(0x12));
|
||||
public Task<ErrorStatus> GetErrorStatusAsync() => Task.FromResult(ErrorStatus.Parse(0x12));
|
||||
public Task<PaperSensorStatus> GetPaperSensorStatusAsync() => Task.FromResult(PaperSensorStatus.Parse(0x12));
|
||||
public Task<byte> GetTM220StatusAsync() => Task.FromResult((byte)0);
|
||||
public Task<byte?> GetPrinterIdAsync() => Task.FromResult<byte?>(0x20);
|
||||
public Task<OverallStatus> GetOverallStatusAsync() => Task.FromResult(new OverallStatus());
|
||||
public Task InitAsync() => Task.CompletedTask;
|
||||
public Task SetQuadrupleMode(bool enable) => Task.CompletedTask;
|
||||
public Task SetRedColor(bool enable) => Task.CompletedTask;
|
||||
public Task SetEmphasized(bool enable) => Task.CompletedTask;
|
||||
public Task SelectFont(EpsonCommands.PrinterFont font) => Task.CompletedTask;
|
||||
public Task SetBiggerFontTM220(bool biggerWidth, bool biggerHeight = false, bool secondaryFont = true) => Task.CompletedTask;
|
||||
public Task SetFontSizeAsync(int width, int height) => Task.CompletedTask;
|
||||
public Task PrintTextAndCutAsync(string text, int feedLines = 5) => Task.CompletedTask;
|
||||
public Task PrintBuffer() => Task.CompletedTask;
|
||||
public Task LoadImageAsync(Stream imageStream, int maxWidth = 384) => Task.CompletedTask;
|
||||
public Task LoadImageAsync(Image<Rgba32> image, int maxWidth = 384) => Task.CompletedTask;
|
||||
public Task PrintLoadedImage() => Task.CompletedTask;
|
||||
public Task PrintImageBitModeAsync(Stream imageStream, EpsonCommands.BitImageMode mode = EpsonCommands.BitImageMode.DoubleDensity24Dot, int maxWidth = 384) => Task.CompletedTask;
|
||||
public Task PrintImageBitModeAsync(Image<Rgba32> image, EpsonCommands.BitImageMode mode, int maxWidth) => Task.CompletedTask;
|
||||
public Task SetDefaultLineSpacing() => Task.CompletedTask;
|
||||
public Task SetCustomLineSpacing(int spacing = 24) => Task.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
}
|
||||
96
Inspectron.Epson/PrintServer/Printers/POS895.cs
Normal file
96
Inspectron.Epson/PrintServer/Printers/POS895.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using Inspectron.Epson.PrintServer.Printers.Utils;
|
||||
using Inspectron.Epson.PrintServer.PrintServices;
|
||||
|
||||
namespace Inspectron.Epson.PrintServer.Printers;
|
||||
|
||||
/// <summary>
|
||||
/// Adapter for the POS895 / POS896, SPRT-family ESC/POS clones on an 80 mm,
|
||||
/// 576-dot line (Font A = 48 columns).
|
||||
///
|
||||
/// Cloned from TM_T30IIITranslated with three differences, each confirmed on
|
||||
/// hardware and recorded in POS895_POS896_integration_notes.md:
|
||||
///
|
||||
/// 1. ESC d n is ignored by this firmware, so feeding uses literal LF bytes.
|
||||
/// Using ESC d would cut the receipt through its last line.
|
||||
/// 2. GS ( L prints nothing, so the logo goes out as ESC * bit-image.
|
||||
/// 3. The line is 576 dots, not 384, and the logo spans it from position 0,
|
||||
/// which avoids ESC $ (still unverified here).
|
||||
/// </summary>
|
||||
public class POS895 : IPrinter
|
||||
{
|
||||
/// <summary>Dots per line. Font A is 12 dots wide, giving 48 columns.</summary>
|
||||
public const int PaperWidthDots = 576;
|
||||
|
||||
private const byte LineFeed = 0x0A;
|
||||
private const int FeedLinesBeforeCut = 10;
|
||||
|
||||
private readonly IEpsonPrinter _printer;
|
||||
|
||||
public bool SkipDelays { get; set; }
|
||||
|
||||
public POS895(IEpsonPrinter printer)
|
||||
{
|
||||
_printer = printer;
|
||||
}
|
||||
|
||||
public Task InitAsync()
|
||||
{
|
||||
return _printer.InitAsync();
|
||||
}
|
||||
|
||||
public async Task PrintImageAsync(string path)
|
||||
{
|
||||
await FeedAsync(1);
|
||||
await _printer.PrintImageBitModeAsync(
|
||||
path, EpsonCommands.BitImageMode.DoubleDensity24Dot, PaperWidthDots);
|
||||
if (!SkipDelays) await Task.Delay(100);
|
||||
await FeedAsync(1);
|
||||
}
|
||||
|
||||
public async Task PrintAsync(List<PrintCommand> commands)
|
||||
{
|
||||
await _printer.SetCustomLineSpacing(22);
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
if (command.IsCut)
|
||||
{
|
||||
await FeedAsync(FeedLinesBeforeCut);
|
||||
await _printer.CutAsync(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (command.SetLineSpacing.HasValue)
|
||||
{
|
||||
await _printer.SetCustomLineSpacing(command.SetLineSpacing.Value);
|
||||
continue;
|
||||
}
|
||||
|
||||
await _printer.SetFontSizeAsync(command.IsBig ? 2 : 1, command.IsTall ? 2 : 1);
|
||||
await _printer.SetEmphasized(command.IsBold);
|
||||
await _printer.PrintTextAsync(command.Text + "\n");
|
||||
}
|
||||
|
||||
await _printer.SetDefaultLineSpacing();
|
||||
await FeedAsync(FeedLinesBeforeCut);
|
||||
if (!SkipDelays) await Task.Delay(200);
|
||||
}
|
||||
|
||||
public async Task Cut()
|
||||
{
|
||||
await _printer.CutAsync();
|
||||
if (!SkipDelays) await Task.Delay(200);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Feeds with literal LF bytes. IEpsonPrinter.FeedLinesAsync emits ESC d,
|
||||
/// which this firmware ignores, and the Epson models depend on that method
|
||||
/// staying as it is.
|
||||
/// </summary>
|
||||
private Task FeedAsync(int lines)
|
||||
{
|
||||
var bytes = new byte[lines];
|
||||
Array.Fill(bytes, LineFeed);
|
||||
return _printer.SendRawCommandAsync(bytes);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user