<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BLE Printer Test</title>
<script src="https://cdn.jsdelivr.net/npm/iconv-lite-umd@0.6.10/lib/iconv-lite-umd.js"></script>
</head>
<body>
<h1>BLE Printer Test 123</h1>
<button id="printTestButton">Test Print</button>
<button id="printAgain">Print Again</button>
<pre id="log"></pre>
<script>
// Function to log messages on the page
function logMessage(message) {
const logElement = document.getElementById('log');
logElement.textContent += message + '\n';
}
var device;
var server;
var service;
var characteristic;
var name = 'T58_6752'; // 藍芽設備的名稱
var uuid = 0x1800; //service UUID (用 BLE Scanner 找到的)
var characteristicUuid = 0x2A00; // characteristic UUID (用 BLE Scanner 找到的)
// Function to connect to BLE printer and send test data
async function connectAndTestPrint() {
try {
logMessage("Requesting Bluetooth device...");
// Request the BLE device
device = await navigator.bluetooth.requestDevice({
//acceptAllDevices: true,
filters: [
{ name: name }
],
optionalServices: [uuid] // Replace with the correct service UUID
});
logMessage(`Device selected: ${device.name}`);
logMessage(`Device uuid: ${device.id}`);
await printAgain();
} catch (error) {
logMessage(`Error: ${error.message}`);
}
}
// 转码方法
function stringToGbk(str) {
const ranges = [
[0xA1, 0xA9, 0xA1, 0xFE],
[0xB0, 0xF7, 0xA1, 0xFE],
[0x81, 0xA0, 0x40, 0xFE],
[0xAA, 0xFE, 0x40, 0xA0],
[0xA8, 0xA9, 0x40, 0xA0],
[0xAA, 0xAF, 0xA1, 0xFE],
[0xF8, 0xFE, 0xA1, 0xFE],
[0xA1, 0xA7, 0x40, 0xA0],
]
const codes = new Uint16Array(23940)
let i = 0
for (const [b1Begin, b1End, b2Begin, b2End] of ranges) {
for (let b2 = b2Begin; b2 <= b2End; b2++) {
if (b2 !== 0x7F) {
for (let b1 = b1Begin; b1 <= b1End; b1++) {
codes[i++] = b2 << 8 | b1
}
}
}
}
const cstr = new TextDecoder('gbk').decode(codes)
// 编码表
const table = new Uint16Array(65536)
for (let i = 0; i < cstr.length; i++) {
table[cstr.charCodeAt(i)] = codes[i]
}
const buf = new Uint8Array(str.length * 2)
let n = 0
for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i)
if (code < 0x80) {
buf[n++] = code
} else {
const gbk = table[code]
buf[n++] = gbk & 0xFF
buf[n++] = gbk >> 8
}
}
u8buf = buf.subarray(0, n)
// console.log(u8buf);
return u8buf
}
async function printAgain() {
// Connect to the GATT server
server = await device.gatt.connect();
logMessage("Connected to GATT server.");
// Get the printer service
service = await server.getPrimaryService(uuid); // Replace with your printer's service UUID
logMessage("Printer service retrieved.");
// Get the characteristic for writing data
characteristic = await service.getCharacteristic(characteristicUuid); // Replace with the correct characteristic UUID
logMessage("Printer characteristic retrieved.");
// Prepare test print data
const encoder = new TextEncoder();
const testData = encoder.encode("TEST PRINT: Hello from Web Bluetooth!\n");
const finalData = encoder.encode("--\n--\n \n \n");
const setFontSize = new Uint8Array([0x1D, 0x21, 0x11]); // GS ! n
const setFontSize2 = new Uint8Array([0x1D, 0x21, 0x22]); // GS ! n
const setFontSize3 = new Uint8Array([0x1D, 0x21, 0x33]); // GS ! n
// Write test data to the printer
logMessage("Sending test data to printer...");
await characteristic.writeValue(new Uint8Array([0x1D, 0x21, 0x00]));
await characteristic.writeValue(encoder.encode("1x1!\n"));
await characteristic.writeValue(setFontSize);
await characteristic.writeValue(encoder.encode("2x2!\n"));
await characteristic.writeValue(setFontSize2);
await characteristic.writeValue(encoder.encode("3x3!\n"));
await characteristic.writeValue(setFontSize3);
await characteristic.writeValue(encoder.encode("4x4!\n"));
const initPrinter = new Uint8Array([0x1B, 0x40]); // ESC @
await characteristic.writeValue(initPrinter);
// 3. 設置字符集為 GBK
const setGBK = new Uint8Array([0x1B, 0x74, 0x11]); // ESC t 0x11 (GBK)
await characteristic.writeValue(setGBK);
const text = "繁體中文測試\n \n";
const encodedText = stringToGbk(text);
await characteristic.writeValue(encodedText);
logMessage("Test data sent successfully!");
// QrCode 列印
const qrData = "https://example.com"; // Your QR code data
const qrDataLength = qrData.length + 3;
const pL = qrDataLength & 0xFF; // Low byte
const pH = (qrDataLength >> 8) & 0xFF; // High byte
const commands = [
0x1B, 0x40, // Initialize printer
0x1D, 0x28, 0x6B, pL, pH, 0x31, 0x50, 0x30, ...new TextEncoder().encode(qrData), // Store data
0x1D, 0x28, 0x6B, 0x03, 0x00, 0x31, 0x51, 0x30 // Print QR code
];
const buffer = new Uint8Array(commands);
await characteristic.writeValue(buffer);
logMessage("QrCode sent successfully!");
await characteristic.writeValue(finalData);
logMessage("finalData sent successfully!");
// Disconnect the GATT server
server.disconnect();
logMessage("Disconnected from printer.");
}
// Bind the function to the button
document.getElementById('printTestButton').addEventListener('click', connectAndTestPrint);
document.getElementById('printAgain').addEventListener('click', printAgain);
</script>
</body>
</html>
<ItemGroup>
<PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
<PackageReference Include="SQLiteNetExtensions.Async" Version="2.1.0" />
<PackageReference Include="SQLitePCLRaw.bundle_green" Version="2.1.4" />
<PackageReference Include="SQLitePCLRaw.core" Version="2.1.4" />
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="2.1.4" />
<PackageReference Include="SQLitePCLRaw.provider.dynamic_cdecl" Version="2.1.4" />
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
</ItemGroup>
static List<Brush> CaptchaBrushes = null;
public static FileStreamResult CreateCaptcha(string captcha)
{
if (CaptchaBrushes == null)
{
CaptchaBrushes = new List<Brush>();
CaptchaBrushes.Add(Brushes.White);
CaptchaBrushes.Add(Brushes.Gold);
CaptchaBrushes.Add(Brushes.LightSkyBlue);
CaptchaBrushes.Add(Brushes.LimeGreen);
CaptchaBrushes.Add(Brushes.AliceBlue);
CaptchaBrushes.Add(Brushes.AntiqueWhite);
CaptchaBrushes.Add(Brushes.BurlyWood);
CaptchaBrushes.Add(Brushes.Silver);
}
int width = 90;
int height = 45;
//https://stackoverflow.com/questions/61365732/cannot-access-a-closed-stream-when-returning-filestreamresult-from-c-sharp-netc
//Using statements close and unload the variable from memory set in the using statement which is why you are getting an error trying to access a closed memory stream.You don't need to use a using statement if you are just going to return the result at the end.
//這個 memory stream 不用關閉或 dispose
var ms = new MemoryStream();
// 釋放所有在 GDI+ 所佔用的記憶體空間 ( 非常重要!! )
using (Bitmap _bmp = new Bitmap(width, height))
using (Graphics _graphics = Graphics.FromImage(_bmp))
using (Font _font = new Font("Courier New", 24, FontStyle.Bold)) // _font 設定要出現在圖片上的文字字型、大小與樣式
{
// (封裝 GDI+ 繪圖介面) 所有繪圖作業都需透過 Graphics 物件進行操作
_graphics.Clear(Color.Black);
// 如果想啟用「反鋸齒」功能,可以將以下這行取消註解
//_graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
// 將亂碼字串「繪製」到之前產生的 Graphics 「繪圖板」上
var x = 10;
for(var i = 0; i < captcha.Length; i++)
{
_graphics.DrawString(captcha.Substring(i, 1), _font, CaptchaBrushes[Su.MathUtil.GetRandomInt(CaptchaBrushes.Count)], x, Su.MathUtil.GetRandomInt(15));
x += 10 + Su.MathUtil.GetRandomInt(10);
}
// 畫線
_graphics.DrawLine(new Pen(CaptchaBrushes[Su.MathUtil.GetRandomInt(CaptchaBrushes.Count)], 1),
Su.MathUtil.GetRandomInt(0, Convert.ToInt32((width * 0.9 / 2))), 0, Su.MathUtil.GetRandomInt(Convert.ToInt32(width / 2), Convert.ToInt32(width * 1.9 / 2)), height);
_graphics.DrawLine(new Pen(CaptchaBrushes[Su.MathUtil.GetRandomInt(CaptchaBrushes.Count)], 1),
Su.MathUtil.GetRandomInt(Convert.ToInt32(width / 2), Convert.ToInt32(width * 1.9 / 2)), 0, Su.MathUtil.GetRandomInt(0, Convert.ToInt32((width * 0.9 / 2))), height);
_graphics.DrawLine(new Pen(CaptchaBrushes[Su.MathUtil.GetRandomInt(CaptchaBrushes.Count)], 1),
0,
Su.MathUtil.GetRandomInt(height / 2),
width,
height / 2 + Su.MathUtil.GetRandomInt(height / 2)
);
_graphics.DrawLine(new Pen(CaptchaBrushes[Su.MathUtil.GetRandomInt(CaptchaBrushes.Count)], 1),
0,
height / 2 + Su.MathUtil.GetRandomInt(height / 2),
width,
Su.MathUtil.GetRandomInt(height / 2)
);
_bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
}
ms.Seek(0, SeekOrigin.Begin);
// Controller 的型別為 FileResult
return new FileStreamResult(ms, "image/jpeg")
{ FileDownloadName = $"{DateTime.Now.Ymdhmsf()}.jpg" };
}
namespace Web.Controllers
{
public class CaptchaController : Controller
{
[Route("captcha")]
public async Task<FileStreamResult> Index()
{
//產生 Captcha 並存入 Session 之中。目前是四位數字
string captcha = (await Ah.ReGetAsync<object>("api/kol/create-captcha-code")).ToString();
//產生圖檔並回傳 FileStreamResult
return Su.Wu.CreateCaptcha(captcha);
}
}
}
public class UploadController : ApiController
{
public async Task<object> PostFormData()
{
var provider = new MultipartMemoryStreamProvider();
if (! Request.Content.IsMimeMultipartContent())
{
return "no file";
}
//要注意這裡的 await
await Request.Content.ReadAsMultipartAsync(provider);
foreach (var content in provider.Contents)
{
if (content.Headers.ContentDisposition.FileName != null)
{
string localFilename = content.Headers.ContentDisposition.FileName.Replace("\"", "");
System.IO.Directory.CreateDirectory(HttpContext.Current.Server.MapPath(@"~/App_Data/Temp/"));
string filename = HttpContext.Current.Server.MapPath(@"~/App_Data/Temp/" + localFilename);
if (System.IO.File.Exists(filename))
{
System.IO.File.Delete(filename);
}
using (var fileStream = new FileStream(filename, FileMode.Create, FileAccess.Write))
{
var contentStream = await content.ReadAsStreamAsync();
await contentStream.CopyToAsync(fileStream);
Trace.WriteLine("Save To" + filename);
}
}
}
return "OK";
}
}
/// <summary>
/// 檔案檢查
/// </summary>
/// <param name="actionContext"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override async Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
{
Trace.WriteLine("ApiCheckFile OnActionExecutingAsync");
var request = actionContext.Request;
if (!request.Content.IsMimeMultipartContent())
{
return;
}
var provider = new MultipartMemoryStreamProvider();
await request.Content.ReadAsMultipartAsync(provider);
//把 provider 存入 System.Web.HttpContext.Current.Items 之中,以便在 controller 中再度使用
System.Web.HttpContext.Current.Items["MimeMultipartContentProvider"] = provider;
foreach (var content in provider.Contents)
{
if (content.Headers.ContentDisposition.FileName != null)
{
var filename = content.Headers.ContentDisposition.FileName.Replace("\"", "");
Trace.WriteLine(filename);
var ext = System.IO.Path.GetExtension(filename);
if (!".jpg,.jpeg,.png".Contains(ext.ToLower()))
{
throw new Exception("file format error.");
}
}
}
return;
}
public async Task<object> PostFormData()
{
//改由 HttpContext.Current.Items 中,讀取資料。
MultipartMemoryStreamProvider provider = (MultipartMemoryStreamProvider)System.Web.HttpContext.Current.Items["MimeMultipartContentProvider"];
//如果沒有經過 filter,provider會是 null, 這時就要直接由 Request.Content 讀入 provider
if (provider == null)
{
provider = new MultipartMemoryStreamProvider();
Request.Content.ReadAsMultipartAsync(provider);
}
foreach (var content in provider.Contents)
{
if (content.Headers.ContentDisposition.FileName != null)
{
string localFilename = content.Headers.ContentDisposition.FileName.Replace("\"", "");
Trace.WriteLine("FileName: " + localFilename);
Trace.WriteLine("FileName: " + @"~/App_Data/Temp/" + localFilename);
System.IO.Directory.CreateDirectory(HttpContext.Current.Server.MapPath(@"~/App_Data/Temp/"));
string filename = HttpContext.Current.Server.MapPath(@"~/App_Data/Temp/" + localFilename);
if (System.IO.File.Exists(filename))
{
System.IO.File.Delete(filename);
}
using (var fileStream = new FileStream(filename, FileMode.Create, FileAccess.Write))
{
var contentStream = await content.ReadAsStreamAsync();
await contentStream.CopyToAsync(fileStream);
Trace.WriteLine("Save To" + filename);
}
}
else
{
var contentStream = await content.ReadAsStreamAsync();
var reader = new System.IO.StreamReader(contentStream);
var data = reader.ReadToEnd();
Trace.WriteLine("data: " + data);
}
}
return "OK";
}
public override void OnActionExecuting(ActionExecutingContext actionContext)
{
Debug.WriteLine("MvcCheckFileFilter OnActionExecuting");
if (actionContext.HttpContext.Request.Files.Count > 0)
{
for (int i = 0; i < actionContext.HttpContext.Request.Files.Count; i++)
{
System.Web.HttpPostedFileBase file = actionContext.HttpContext.Request.Files[i];
if (System.IO.Path.GetExtension(file.FileName) != ".jpg")
{
throw new Exception("file format error.");
}
Debug.WriteLine(i + "MvcCheckFileFilter OnActionExecuting File type: " + file.FileName.ToString());
}
}
//以下寫法會發生錯誤: 無法將類型 'System.String' 的物件轉換為類型 'System.Web.HttpPostedFileBase'。
//foreach (HttpPostedFileBase file in actionContext.HttpContext.Request.Files)
//{
// if (System.IO.Path.GetExtension(file.FileName) != ".jpg")
// {
// throw new Exception("file format error.");
// }
//}
}