diff --git a/Inspectron.Epson.slnx b/Inspectron.Epson.slnx
index 6cb719b..875148b 100644
--- a/Inspectron.Epson.slnx
+++ b/Inspectron.Epson.slnx
@@ -2,6 +2,7 @@
+
diff --git a/Pos895Test/Pos895Test.csproj b/Pos895Test/Pos895Test.csproj
new file mode 100644
index 0000000..fef97ee
--- /dev/null
+++ b/Pos895Test/Pos895Test.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/Pos895Test/Program.cs b/Pos895Test/Program.cs
new file mode 100644
index 0000000..87e73d8
--- /dev/null
+++ b/Pos895Test/Program.cs
@@ -0,0 +1,160 @@
+using System.Text;
+using Inspectron.Epson;
+
+// POS895 smoke test: Font A column count vs. GS ! width magnification.
+//
+// Prints one ruler row per width magnification, each at 48/width characters, so
+// every row should end at the same right margin with progressively larger type.
+// A row that stops short means that magnification was ignored.
+//
+// All settings are the constants below. Edit them here; there are no arguments.
+
+// An AnyDesk TCP tunnel forwards this local endpoint to the remote printer's
+// port 9100, so the printer is reachable as though it sat on this machine.
+// A refused connection here usually means the AnyDesk session or its tunnel is
+// down, not that the printer is off.
+const string PrinterIp = "127.0.0.1";
+const int PrinterPort = 8888;
+const int ConnectTimeoutSeconds = 5;
+
+// Font A is 12 dots wide on a 576-dot line. Width magnification divides this.
+const int FontAColumns = 48;
+const int HeightMagnification = 2;
+int[] widths = { 1, 2, 3 };
+
+// Blank lines between the last row and the cut. Literal LF bytes rather than
+// ESC d n: LF is the most universally implemented feed in ESC/POS, and this
+// firmware appears to ignore ESC d.
+const int BlankLinesAfterText = 10;
+
+// Set true to drop the ESC t 18 (code page 18 / CP852) selection. The ruler is
+// pure ASCII either way; this only isolates whether ESC t itself is honoured.
+bool useRawAscii = false;
+
+byte[] job = BuildJob(FontAColumns, widths, HeightMagnification, BlankLinesAfterText, useRawAscii);
+
+Console.WriteLine($"POS895 smoke test -> {PrinterIp}:{PrinterPort}");
+Console.WriteLine($"Font A, height {HeightMagnification}x, widths {string.Join("x / ", widths)}x");
+Console.WriteLine();
+foreach (int w in widths)
+{
+ int cols = FontAColumns / w;
+ Console.WriteLine($" GS ! 0x{SizeByte(w, HeightMagnification):X2} w={w} h={HeightMagnification} {cols,2} cols {Ruler(cols)}");
+}
+Console.WriteLine();
+Console.WriteLine("Byte sequence:");
+Console.WriteLine(Describe(job));
+Console.WriteLine();
+
+try
+{
+ await using var printer = new EpsonPrinter();
+
+ Console.Write("Connecting... ");
+ await printer.ConnectAsync(PrinterIp, PrinterPort, ConnectTimeoutSeconds);
+ Console.WriteLine("connected.");
+
+ Console.Write($"Sending {job.Length} bytes... ");
+ await printer.SendRawCommandAsync(job);
+ Console.WriteLine("sent.");
+
+ Console.WriteLine();
+ Console.WriteLine("[OK] Bytes sent. All three rows should end at the same right");
+ Console.WriteLine(" margin, each in larger type than the one above it.");
+ return 0;
+}
+catch (EpsonConnectionException ex)
+{
+ Console.Error.WriteLine();
+ Console.Error.WriteLine($"[FAIL] Could not reach {PrinterIp}:{PrinterPort}.");
+ Console.Error.WriteLine(" Is the port forward to the printer's 9100 up?");
+ Console.Error.WriteLine($" {ex.Message}");
+ if (ex.InnerException is not null)
+ {
+ Console.Error.WriteLine($" {ex.InnerException.Message}");
+ }
+ return 1;
+}
+catch (Exception ex)
+{
+ Console.Error.WriteLine();
+ Console.Error.WriteLine($"[FAIL] {ex.GetType().Name}: {ex.Message}");
+ return 1;
+}
+
+// The whole job as one array, so what gets listed above is exactly what goes on
+// the wire.
+static byte[] BuildJob(int fontACols, int[] widths, int heightMag, int blankLines, bool rawAscii)
+{
+ var job = new List { 0x1B, 0x40 }; // ESC @ initialize
+
+ if (!rawAscii)
+ {
+ job.AddRange(new byte[] { 0x1B, 0x74, 0x12 }); // ESC t 18 code page 852
+ }
+
+ job.AddRange(new byte[] { 0x1B, 0x21, 0x00 }); // ESC ! 0 Font A, no attributes
+
+ foreach (int w in widths)
+ {
+ job.AddRange(new byte[] { 0x1D, 0x21, SizeByte(w, heightMag) });
+ job.AddRange(Encoding.ASCII.GetBytes(Ruler(fontACols / w)));
+ job.Add(0x0A);
+ }
+
+ job.AddRange(new byte[] { 0x1D, 0x21, 0x00 }); // GS ! 0 back to 1x1
+
+ for (int i = 0; i < blankLines; i++)
+ {
+ job.Add(0x0A); // blank line
+ }
+
+ job.AddRange(new byte[] { 0x1D, 0x56, 0x00 }); // GS V 0 full cut
+ return job.ToArray();
+}
+
+// GS ! n: bits 4-6 are width magnification - 1, bits 0-2 height - 1. Same
+// formula as EpsonPrinter.SetFontSizeAsync.
+static byte SizeByte(int width, int height) =>
+ (byte)(((height - 1) & 0x07) | (((width - 1) & 0x07) << 4));
+
+// "|---" repeated and cut to an exact column count, so the pipes fall every
+// 4 characters and the row ends precisely at the claimed right margin.
+static string Ruler(int columns)
+{
+ var sb = new StringBuilder();
+ while (sb.Length < columns)
+ {
+ sb.Append("|---");
+ }
+
+ return sb.ToString(0, columns);
+}
+
+// Hex plus a printable-ASCII gutter, so the output can be compared against the
+// printer's hex dump mode (open cover, FEED twice within 3 s, close cover).
+static string Describe(byte[] bytes)
+{
+ var sb = new StringBuilder();
+ for (int offset = 0; offset < bytes.Length; offset += 16)
+ {
+ int count = Math.Min(16, bytes.Length - offset);
+ sb.Append($" {offset:X4} ");
+
+ for (int i = 0; i < 16; i++)
+ {
+ sb.Append(i < count ? $"{bytes[offset + i]:X2} " : " ");
+ }
+
+ sb.Append(' ');
+ for (int i = 0; i < count; i++)
+ {
+ byte b = bytes[offset + i];
+ sb.Append(b >= 0x20 && b < 0x7F ? (char)b : '.');
+ }
+
+ sb.AppendLine();
+ }
+
+ return sb.ToString().TrimEnd();
+}