部署在本地的打印伴侣服务,将服务作为一台设备连接至阿里云物联网平台,实现远程调用本地打印服务,由打印服务发起本地打印机工作调用。
思路:
1、NetworkImageServer 实现本地打印服务,启动工程时自动启动打印服务
2、MQTTServer 连接阿里云物联网平台,实现远程调用本地打印服务,topic发布消息即可
3、选择并设置默认打印机,在ManageController.cs中实现,保存到client.config.json文件
4、本地调用打印服务可通过打印服务API,在 ImageController.cs中实现
第一步:创建网络图片打印服务
NetworkImageServer.cs
using GeekOpen.Common;
using GeekOpen.Core;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Printing;
using System.Linq;
using System.Threading.Tasks;
namespace PrintMateClient.Server
{
/// <summary>
/// 打印的图片信息
/// </summary>
public class ImageParameter
{
public string url { get; set; }
}
/// <summary>
/// 打印任务数据包
/// </summary>
public class ImagePrintTask
{
/// <summary>
/// 是否转为横向
/// </summary>
public bool landscape { get; set; }
/// <summary>
/// 纸张宽度,单位 mm
/// </summary>
public int width { get; set; }
/// <summary>
/// 纸张高度,单位 mm
/// </summary>
public int height { get; set; }
/// <summary>
/// 打印机X轴偏移量,单位 mm
/// </summary>
public int offsetX { get; set; }
/// <summary>
/// 打印机Y轴偏移量,单位 mm
/// </summary>
public int offsetY { get; set; }
/// <summary>
/// 需要打印的图片列表
/// </summary>
public List<ImageParameter> imageParameters { get; set; }
}
/// <summary>
/// 网络图片打印服务
/// </summary>
public class NetworkImageServer
{
public List<ImageParameter> imageParameters;
public ImageParameter currentParameter;
PrintDocument print;
ImagePrintTask imagePrintTask;
string printName;
/// <summary>
/// 网络图片打印服务
/// </summary>
/// <param name="printName">本地打印机名称</param>
/// <param name="imagePrintTask"></param>
public NetworkImageServer(string printName,ImagePrintTask imagePrintTask)
{
this.imagePrintTask = imagePrintTask;
this.imageParameters = imagePrintTask.imageParameters;
this.printName = printName;
bool toPrint = false;
string[] systemPrints = { };
foreach (string sPrint in PrinterSettings.InstalledPrinters)//获取所有打印机名称
{
if(sPrint== this.printName)
{
toPrint = true;
}
systemPrints.Append(sPrint);
}
if(toPrint)
{
//转换纸张尺寸,如果未设定,则默认A4打印
this.imagePrintTask.width = this.imagePrintTask.width > 0 ? P(this.imagePrintTask.width) : P(2100);
this.imagePrintTask.height = this.imagePrintTask.height > 0 ? P(this.imagePrintTask.height) : P(2970);
print = new PrintDocument();
print.PrinterSettings.PrinterName = this.printName;//读取本地配置中的打印机名称,如:"Deli DL-888B(NEW)"
print.DefaultPageSettings.Landscape = imagePrintTask.landscape; //设置横向打印,不设置默认false是纵向的
print.DefaultPageSettings.PaperSize = new PaperSize("Custom Size 1", this.imagePrintTask.width, this.imagePrintTask.height);
print.PrintController = new StandardPrintController();
}
else
{
throw new BusException("Cleint没有找到打印机:"+ this.printName,true, "systemPrints="+ String.Join(";",systemPrints));
}
}
/// <summary>
/// 打印多张图片
/// </summary>
public void Print()
{
print.PrintPage += new PrintPageEventHandler(printImageList);
print.Print();
}
/// <summary>
/// 打印多张图片
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void printImageList(object sender, PrintPageEventArgs e)
{
if(imageParameters.Count>0)
{
ImageParameter parameter = imageParameters.Take(1).SingleOrDefault();
Console.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") + " - printImageList --> " + parameter.url);
Image image = new Bitmap(Util.DownloadFile(parameter.url));
e.Graphics.DrawImage(image, 0, 0, 820, 1170);
imageParameters.Remove(parameter);
if (imageParameters.Count>0)
{
e.HasMorePages = true;
}
return;
}
e.HasMorePages = false;
}
/// <summary>
/// 尺寸转换
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
private int P(int v)
{
return int.Parse((v * 1000/254).ToString("F0"));
}
}
}
第二步,将服务通过MQTT连接至阿里云物联网平台
MQTTServer.cs
using GeekOpen.Common;
using GeekOpen.Core;
using GeekOpen.Iot.Common.Values.IoT;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using uPLibrary.Networking.M2Mqtt;
using uPLibrary.Networking.M2Mqtt.Messages;
using System.Runtime.InteropServices;
using System.Threading;
namespace PrintMateClient.Server
{
public static class MQTTState
{
public static bool state = false;
}
public class MQTTServer
{
MqttClient mqttClient;
PrintMateClientInfo printMateClientInfo;
//MQTT消息订阅,执行打印任务
public string printTopicReply;
public MQTTServer()
{
if(!MQTTState.state)
{
ClientConfig clientConfig = ConfigServices.GetClientConfig();
if (!string.IsNullOrEmpty(clientConfig.clientCode) && !string.IsNullOrEmpty(clientConfig.clientSecret))
{
//根据 ClientCode 与 ClientSecret 向 IoT服务获取 productKey、deviceName、deviceSecret
ResultValue<PrintMateClientInfo> clientInfoResult = ConfigServices.GetPrintMateClientInfo();
if(clientInfoResult.success)
{
printMateClientInfo = clientInfoResult.value;
Console.WriteLine("SmartBird -> "+ Util.ToJson(printMateClientInfo));
//执行打印任务 Topic
printTopicReply = "/" + printMateClientInfo.productKey + "/" + printMateClientInfo.deviceName + "/user/print";
Init();
}
else
{
Console.WriteLine("SmartBird -> 无法启动MQTTServer:" + clientInfoResult.message);
}
}
else
{
Console.WriteLine("SmartBird -> 缺少ClientCode 与 ClientSecret配置,无法启动MQTTServer!");
}
}
else
{
Console.WriteLine("SmartBird -> MQTTServer已启动,无需重复启动!");
}
}
MqttSign sign;
private void Init()
{
// 计算MQTT连接参数。
sign = new MqttSign();
sign.calculate(printMateClientInfo.productKey, printMateClientInfo.deviceName, printMateClientInfo.deviceSecret);
Console.WriteLine("username: " + sign.getUsername());
Console.WriteLine("password: " + sign.getPassword());
Console.WriteLine("clientid: " + sign.getClientid());
Connect();
OnClosed();
first();
Subscribe();
Publish();
}
private void OnClosed()
{
mqttClient.ConnectionClosed += Default_ConnectionClosedEventHandler;
}
private void Connect(){
try{
// 使用Paho连接阿里云物联网平台。
int port = 443;
String broker = printMateClientInfo.productKey + ".iot-as-mqtt.cn-shanghai.aliyuncs.com";
mqttClient = new MqttClient(broker, port, true, MqttSslProtocols.TLSv1_2, null, null);
mqttClient.Connect(sign.getClientid(), sign.getUsername(), sign.getPassword());
Console.WriteLine("Broker: " + broker + " Connected");
MQTTState.state = true;
}
catch(Exception e)
{
Console.WriteLine("SmartBirt -> "+DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")+" Connect Error:"+e.Message);
Thread.Sleep(30000);//等待30秒重新连接
Connect();
}
}
/// <summary>
///
/// </summary>
private void first()
{
String topic = "/sys/" + printMateClientInfo.productKey + "/" + printMateClientInfo.deviceName + "/thing/event/property/post";
String message = "{\"id\":\"1\",\"version\":\"1.0\",\"params\":{\"LightSwitch\":0}}";
mqttClient.Publish(topic, Encoding.UTF8.GetBytes(message));
}
/// <summary>
/// 客户端订阅打印消息
/// </summary>
private void Subscribe()
{
mqttClient.MqttMsgPublishReceived += MqttPostProperty_MqttMsgPublishReceived;
mqttClient.Subscribe(new string[] { printTopicReply }, new byte[] { MqttMsgBase.QOS_LEVEL_AT_MOST_ONCE });
}
/// <summary>
/// 客户端订阅打印消息
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MqttPostProperty_MqttMsgPublishReceived(object sender, uPLibrary.Networking.M2Mqtt.Messages.MqttMsgPublishEventArgs e)
{
string topic = e.Topic;
string message = Encoding.UTF8.GetString(e.Message);
Console.WriteLine("reply topic :" + topic);
Console.WriteLine("reply payload:" + message);
try
{
//如果topic是执行打印任务的消息
if(topic == printTopicReply)
{
ImagePrintTask imagePrintTask = Util.JsonTo<ImagePrintTask>(message);
NetworkImageServer imageServer = new NetworkImageServer(printMateClientInfo.printName,imagePrintTask);
imageServer.Print();
Log.Info(printMateClientInfo.clientCode + ",订阅到执行打印任务", "topic=" + topic + ";imagePrintTask="+Util.ToJson(imagePrintTask));
}
}
catch(Exception ex)
{
Console.WriteLine("Error:"+ex.Message);
}
}
/// <summary>
/// 客户端上报运行信息
/// </summary>
private void Publish()
{
return;
if(MQTTState.state)
{
if(RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
ClientSystemInfo clientSystemInfo = new ClientSystemInfo()
{
temp = float.Parse(System.IO.File.ReadAllText(@"/sys/class/thermal/thermal_zone0/temp")) / 1000
};
string content = Util.ToJson(clientSystemInfo);
mqttClient.Publish("/" + printMateClientInfo.productKey + "/" + printMateClientInfo.deviceName + "/user/info", Encoding.UTF8.GetBytes(content));
Thread.Sleep(1000 * 60);
Publish();
}
else
{
ClientSystemInfo clientSystemInfo = new ClientSystemInfo()
{
temp = 123
};
string content = Util.ToJson(clientSystemInfo);
mqttClient.Publish("/" + printMateClientInfo.productKey + "/" + printMateClientInfo.deviceName + "/user/info", Encoding.UTF8.GetBytes(content));
Thread.Sleep(1000 * 60);
Publish();
}
}
}
/// <summary>
/// 连接发生异常,断网等
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Default_ConnectionClosedEventHandler(object sender, EventArgs e)
{
Console.WriteLine("SmartBirt -> ConnectionClosedEventHandler ");
Console.WriteLine("Connect Closed error,Date: " + DateTime.Now.ToLongTimeString());
Connect();
}
}
class CryptoUtil
{
public static String hmacSha256(String plainText, String key)
{
var encoding = new System.Text.UTF8Encoding();
byte[] plainTextBytes = encoding.GetBytes(plainText);
byte[] keyBytes = encoding.GetBytes(key);
HMACSHA256 hmac = new HMACSHA256(keyBytes);
byte[] sign = hmac.ComputeHash(plainTextBytes);
return BitConverter.ToString(sign).Replace("-", string.Empty);
}
}
public class MqttSign
{
private String username = "";
private String password = "";
private String clientid = "";
public String getUsername() { return this.username; }
public String getPassword() { return this.password; }
public String getClientid() { return this.clientid; }
public bool calculate(String productKey, String deviceName, String deviceSecret)
{
if (productKey == null || deviceName == null || deviceSecret == null)
{
return false;
}
//MQTT用户名
this.username = deviceName + "&" + productKey;
//MQTT密码
String timestamp = Convert.ToInt64((DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalMilliseconds).ToString();
String plainPasswd = "clientId" + productKey + "." + deviceName + "deviceName" +
deviceName + "productKey" + productKey + "timestamp" + timestamp;
this.password = CryptoUtil.hmacSha256(plainPasswd, deviceSecret);
//MQTT ClientId
this.clientid = productKey + "." + deviceName + "|" + "timestamp=" + timestamp +
",_v=paho-c#-1.0.0,securemode=2,signmethod=hmacsha256|";
return true;
}
}
public class ClientSystemInfo
{
public float temp { get; set; }
public int Memory { get; set; }
}
}
第三步,启动服务时,自动启动打印服务与MQTT客户端订阅
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json.Converters;
using System.Threading;
using PrintMateClient.Server;
namespace PrintMateClient
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers().AddNewtonsoftJson(option =>
option.SerializerSettings.Converters.Add(new IsoDateTimeConverter() { DateTimeFormat = "yyyy-MM-dd HH:mm:ss" })
);
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();//使用IHttpContextAccessor,在controller就可以使用httpContext信息
//跨域,添加策略
services.AddCors(options => {
options.AddPolicy("any", builder => { builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); });
});
services.AddMvc(
options =>
{
options.Filters.Add<ExceptionFilter>(); //加入全局异常类
options.Filters.Add<ActionFilter>(); //加入全局监测类
});
new Thread(() =>
{
new MQTTServer();
}).Start();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors("any");//跨域,启用策略
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
//每次服务启动时,添加到日志
Log.Startup("Startup", "");
}
}
}
第四步,实现本地打印服务API
ImageController.cs
using GeekOpen.Common;
using GeekOpen.Core;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using PrintMateClient.Server;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Printing;
using System.Linq;
using System.Threading.Tasks;
namespace PrintMateClient.Controllers
{
[ApiController]
[Route("[controller]/[action]")]
public class ImageController : AuthorizeController
{
public ImageController(IHttpContextAccessor accessor) : base(accessor)
{
}
/// <summary>
/// 打印图片,一般手动调用,自动打印在 MQTTServer
/// </summary>
/// <param name="clientCode"></param>
/// <param name="clientSecret"></param>
/// <param name="imagePrintTask"></param>
/// <returns></returns>
[HttpPost("{clientCode}/{clientSecret}")]
public ResultValue Print(string clientCode,string clientSecret, [FromBody] ImagePrintTask imagePrintTask)
{
if(!string.IsNullOrEmpty(clientCode) && !string.IsNullOrEmpty(clientSecret))
{
if (clientCode == ConfigServices.GetClientConfig().clientCode && clientSecret==ConfigServices.GetClientConfig().clientSecret)
{
NetworkImageServer imageServer = new NetworkImageServer(ConfigServices.GetClientConfig().printName,imagePrintTask);
imageServer.Print();
return new ResultValue();
}
else
{
throw new BusException("clientCode或clientSecret错误!");
}
}
else
{
throw new BusException("clientCode与clientSecret不能为空!");
}
}
}
}
第五步,实现本地打印机管理视图页面
ManageController.cs
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Drawing.Printing;
using System.Linq;
using System.Threading.Tasks;
namespace PrintMateClient.Controllers
{
[ApiController]
[Route("")]
public class ManageController : AuthorizeController
{
public ManageController(IHttpContextAccessor accessor) : base(accessor)
{
}
[HttpGet]
public IActionResult Index()
{
List<PrintInfo> printInfos = new List<PrintInfo>();
foreach (string sPrint in PrinterSettings.InstalledPrinters)//获取所有打印机名称
{
printInfos.Add(new PrintInfo() { printName = sPrint});
}
string message = "";
if (!string.IsNullOrEmpty(Request.Query["success"]))
{
if (Request.Query["success"] == "true")
{
message = "设置成功:" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
}
}
ClientConfigView clientConfigView = new ClientConfigView()
{
message = message,
printInfos = printInfos,
clientConfig = ConfigServices.GetClientConfig()
};
return View(clientConfigView);
}
}
}
client.config.json
{"printName":"HPF439095CA4C7 (HP Laser MFP 131 133 135-138)","clientName":"T490笔记本开发","clientCode":"PrintMateClient202106103819","clientSecret":"Ah0192xW8hS2lo301vdSpKr10DG10375"}
Views/Manage/Index.cshtml
@using PrintMateClient
@model ClientConfigView
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>
打印服务设置 - PrintMateClient 1.0
</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Cache-Control" content="no-cache" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=no" />
<link rel="shortcut icon" href="/favicon.ico" />
<style>
body {
font-family: '微软雅黑',Arial, Helvetica, sans-serif;
margin: 0px;
padding: 0px;
background-color: #f2f5f8;
min-height: 100%;
}
* {
table-layout: fixed;
word-break: break-all;
font-family: '微软雅黑',Arial, Helvetica, sans-serif;
box-sizing: border-box;
font-size: 14px;
line-height: 18px;
}
.hidden {
display: none;
}
span {
display: inline-block;
vertical-align: middle;
}
input {
height: 35px;
}
input[type="submit"] {
background-color: #202125;
color: #fff;
}
select {
height: 35px;
padding: 5px;
}
</style>
</head>
<body style="background-color:#f2f5f8;">
<div style="padding:50px;margin:auto;width:480px;">
<form name="myform" method="post" enctype="multipart/form-data" action="/api/ClientConfig/Update">
<div style="display:flex;align-items:center;padding:0px 0px 20px 0px;">
<div style="flex-grow:1; padding:0px 0px 0px 0px;">
<a href="/"><img src="https://oss-smart-bird.oss-cn-qingdao.aliyuncs.com/geek-open/GeekOpen-logo.png" style="height:40px;" title="理工智控-GeekOpen" /></a>
</div>
<div style="flex-grow:1;font-size:14px;font-weight:bold;text-align:right;padding:3px 0px 0px 0px;">
PrintMateClient 1.0
</div>
</div>
<div style="padding:20px 0px 5px 0px;">打印机名称</div>
<select name="printName" id="printName" style="width: 100%;">
@for (int i = 0; i < Model.printInfos.Count; i++)
{
<option value="@Model.printInfos[i].printName" @if(Model.printInfos[i].printName == Model.clientConfig.printName) { Write(" selected"); }>@Model.printInfos[i].printName</option>
}
</select>
<div style="padding:20px 0px 5px 0px;">打印机描述</div>
<div>
<input type="text" name="clientName" id="clientName" value="@Model.clientConfig.clientName" style="width:100%;" />
</div>
<div style="padding:20px 0px 5px 0px;">ClientCode</div>
<div>
<input type="text" name="clientCode" id="clientCode" value="@Model.clientConfig.clientCode" style="width:100%;" />
</div>
<div style="padding:20px 0px 5px 0px;">ClientSecret</div>
<div>
<input type="text" name="clientSecret" id="clientSecret" value="@Model.clientConfig.clientSecret" style="width:100%;" />
</div>
<div style="font-size:12px;padding:15px 0px 0px 0px;color:#0094ff;text-align:center;">
@Model.message
</div>
<div style="padding:35px 0px 0px 0px;">
<input type="submit" name="mysubmit" value="保存设置" style="width:100%;" />
</div>
<div style="font-size:12px;color:#707275;text-align:center;padding:30px 0px 0px 0px;line-height:25px;">
<a href="http://www.geek-open.com" target="_blank" style="color:#707275;">www.geek-open.com</a>
<br />Copyright © 2020-2023 All rights reserved
</div>
</form>
</div>
</body>
</html>