前言
客户端主要是管控车辆进出场。现场电脑又是千差万别,因此考虑兼容性较好的winform来实现。
客户端需要对接硬件设备包括道闸,地磅、摄像头、光栅、打印机等。
winform使用.net framework 4.5
一、基础功能
需要 nuget 引入:
fishlee.net.simpleupdater 4.3
FFmpeg.AutoGen 3.4
System.Speech 5.0
log4net 2.0
System.ValueTuple 4.5
1. 一台电脑启动一次客户端
using FloorScale.WinForm.Api;
using FloorScale.WinForm.Common;
using FSLib.App.SimpleUpdater;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Windows.Forms;
namespace FloorScale.WinForm
{
static class Program
{
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool SetForegroundWindow(IntPtr hWnd);
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main()
{
using (Mutex mutex = new Mutex(true, "YaoAntWeightFrm", out bool createdNew))
{
if (createdNew)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new FrmLogin());
}
else
{
Process current = Process.GetCurrentProcess();
foreach (Process process in Process.GetProcessesByName(current.ProcessName))
{
if (process.Id != current.Id)
{
// 激活窗口至前端
SetForegroundWindow(process.MainWindowHandle);
break;
}
}
}
}
}
}
}
2. 日志记录
使用log4net
3. 自动升级功能
using FSLib.App.SimpleUpdater;
/// <summary>
/// 找到更新的事件.但在此实例中
/// 找到更新会自动进行处理,所以这里并不需要操作
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public static void AutoUpdatesFound(bool mustWait = false)
{
var clientUri = IniAPI.ReadItemValue("ConstSystem", "UpdateClientUri");
if (string.IsNullOrWhiteSpace(clientUri))
{
return;
}
var updateXml = "update_c.xml";
var fullUrl = $"{clientUri.TrimEnd('/')}/{updateXml}";
if (!HttpClientHelper.HttpIsRun(fullUrl)) return;
var updateTemplateUrl = clientUri.TrimEnd('/') + "/{0}";
if (mustWait)
{
var updater = Updater.CreateUpdaterInstance(updateTemplateUrl, updateXml);
var checkResult = updater.CheckUpdateSync(false);
}
try
{
Updater.CheckUpdateSimple(updateTemplateUrl, updateXml);
}
catch (Exception ex)
{
var msg = new StringBuilder();
msg.AppendLine($"自动升级失败:{fullUrl}");
msg.AppendLine(ex.Message);
LogHelper.Error(msg.ToString());
}
}
二、对接硬件设备
1.对接摄像头
客户要求允许对接多品牌摄像头。因此使用rtsp协议,协议参数从后台获取。
使用FFmpeg,这个比较占用存,好的是响应速度快,没什么拖影。
使用多线程处理抓取图片。
部分代码示例:
using FFmpeg.AutoGen;
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
namespace FloorScale.WinForm.Common
{
public unsafe class tstRtmp
{
/// <summary>
/// 显示图片委托
/// </summary>
/// <param name="bitmap"></param>
public delegate void ShowBitmap(Bitmap bitmap);
/// <summary>
/// 执行控制变量
/// </summary>
bool CanRun;
/// <summary>
/// 对读取的264数据包进行解码和转换
/// </summary>
/// <param name="show">解码完成回调函数</param>
/// <param name="url">播放地址,也可以是本地文件地址</param>
public unsafe void Start(ShowBitmap show, string url)
{
CanRun = true;
Console.WriteLine(@"Current directory: " + Environment.CurrentDirectory);
Console.WriteLine(@"Runnung in {0}-bit mode.", Environment.Is64BitProcess ? @"64" : @"32");
//FFmpegDLL目录查找和设置
FFmpegBinariesHelper.RegisterFFmpegBinaries();
#region ffmpeg 初始化
// 初始化注册ffmpeg相关的编码器
ffmpeg.av_register_all();
ffmpeg.avcodec_register_all();
ffmpeg.avformat_network_init();
Console.WriteLine($"FFmpeg version info: {ffmpeg.av_version_info()}");
#endregion
#region ffmpeg 日志
// 设置记录ffmpeg日志级别
ffmpeg.av_log_set_level(ffmpeg.AV_LOG_VERBOSE);
av_log_set_callback_callback logCallback = (p0, level, format, vl) =>
{
if (level > ffmpeg.av_log_get_level()) return;
var lineSize = 1024;
var lineBuffer = stackalloc byte[lineSize];
var printPrefix = 1;
ffmpeg.av_log_format_line(p0, level, format, vl, lineBuffer, lineSize, &printPrefix);
var line = Marshal.PtrToStringAnsi((IntPtr)lineBuffer);
Console.Write(line);
};
ffmpeg.av_log_set_callback(logCallback);
#endregion
#region ffmpeg 转码
// 分配音视频格式上下文
var pFormatContext = ffmpeg.avformat_alloc_context();
int error;
//打开流
error = ffmpeg.avformat_open_input(&pFormatContext, url, null, null);
if (error != 0) throw new ApplicationException(GetErrorMessage(error));
// 读取媒体流信息
error = ffmpeg.avformat_find_stream_info(pFormatContext, null);
if (error != 0) throw new ApplicationException(GetErrorMessage(error));
// 这里只是为了打印些视频参数
AVDictionaryEntry* tag = null;
while ((tag = ffmpeg.av_dict_get(pFormatContext->metadata, "", tag, ffmpeg.AV_DICT_IGNORE_SUFFIX)) != null)
{
var key = Marshal.PtrToStringAnsi((IntPtr)tag->key);
var value = Marshal.PtrToStringAnsi((IntPtr)tag->value);
Console.WriteLine($"{key} = {value}");
}
// 从格式化上下文获取流索引
AVStream* pStream = null, aStream;
for (var i = 0; i < pFormatContext->nb_streams; i++)
{
if (pFormatContext->streams[i]->codec->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
{
pStream = pFormatContext->streams[i];
}
else if (pFormatContext->streams[i]->codec->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO)
{
aStream = pFormatContext->streams[i];
}
}
if (pStream == null) throw new ApplicationException(@"Could not found video stream.");
// 获取流的编码器上下文
var codecContext = *pStream->codec;
Console.WriteLine($"codec name: {ffmpeg.avcodec_get_name(codecContext.codec_id)}");
// 获取图像的宽、高及像素格式
var width = codecContext.width;
var height = codecContext.height;
var sourcePixFmt = codecContext.pix_fmt;
// 得到编码器ID
var codecId = codecContext.codec_id;
// 目标像素格式
var destinationPixFmt = AVPixelFormat.AV_PIX_FMT_BGR24;
// 某些264格式codecContext.pix_fmt获取到的格式是AV_PIX_FMT_NONE 统一都认为是YUV420P
if (sourcePixFmt == AVPixelFormat.AV_PIX_FMT_NONE && codecId == AVCodecID.AV_CODEC_ID_H264)
{
sourcePixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P;
}
// 得到SwsContext对象:用于图像的缩放和转换操作
var pConvertContext = ffmpeg.sws_getContext(width, height, sourcePixFmt,
width, height, destinationPixFmt,
ffmpeg.SWS_FAST_BILINEAR, null, null, null);
if (pConvertContext == null) throw new ApplicationException(@"Could not initialize the conversion context.");
//分配一个默认的帧对象:AVFrame
var pConvertedFrame = ffmpeg.av_frame_alloc();
// 目标媒体格式需要的字节长度
var convertedFrameBufferSize = ffmpeg.av_image_get_buffer_size(destinationPixFmt, width, height, 1);
// 分配目标媒体格式内存使用
var convertedFrameBufferPtr = Marshal.AllocHGlobal(convertedFrameBufferSize);
var dstData = new byte_ptrArray4();
var dstLinesize = new int_array4();
// 设置图像填充参数
ffmpeg.av_image_fill_arrays(ref dstData, ref dstLinesize, (byte*)convertedFrameBufferPtr, destinationPixFmt, width, height, 1);
#endregion
#region ffmpeg 解码
// 根据编码器ID获取对应的解码器
var pCodec = ffmpeg.avcodec_find_decoder(codecId);
if (pCodec == null) throw new ApplicationException(@"Unsupported codec.");
var pCodecContext = &codecContext;
if ((pCodec->capabilities & ffmpeg.AV_CODEC_CAP_TRUNCATED) == ffmpeg.AV_CODEC_CAP_TRUNCATED)
pCodecContext->flags |= ffmpeg.AV_CODEC_FLAG_TRUNCATED;
// 通过解码器打开解码器上下文:AVCodecContext pCodecContext
error = ffmpeg.avcodec_open2(pCodecContext, pCodec, null);
if (error < 0) throw new ApplicationException(GetErrorMessage(error));
// 分配解码帧对象:AVFrame pDecodedFrame
var pDecodedFrame = ffmpeg.av_frame_alloc();
// 初始化媒体数据包
var packet = new AVPacket();
var pPacket = &packet;
ffmpeg.av_init_packet(pPacket);
var frameNumber = 0;
while (CanRun)
{
try
{
do
{
// 读取一帧未解码数据
error = ffmpeg.av_read_frame(pFormatContext, pPacket);
// Console.WriteLine(pPacket->dts);
if (error == ffmpeg.AVERROR_EOF) break;
if (error < 0) throw new ApplicationException(GetErrorMessage(error));
if (pPacket->stream_index != pStream->index) continue;
// 解码
error = ffmpeg.avcodec_send_packet(pCodecContext, pPacket);
if (error < 0) throw new ApplicationException(GetErrorMessage(error));
// 解码输出解码数据
error = ffmpeg.avcodec_receive_frame(pCodecContext, pDecodedFrame);
} while (error == ffmpeg.AVERROR(ffmpeg.EAGAIN) && CanRun);
if (error == ffmpeg.AVERROR_EOF) break;
if (error < 0) throw new ApplicationException(GetErrorMessage(error));
if (pPacket->stream_index != pStream->index) continue;
//Console.WriteLine($@"frame: {frameNumber}");
// YUV->RGB
ffmpeg.sws_scale(pConvertContext, pDecodedFrame->data, pDecodedFrame->linesize, 0, height, dstData, dstLinesize);
}
finally
{
ffmpeg.av_packet_unref(pPacket);//释放数据包对象引用
ffmpeg.av_frame_unref(pDecodedFrame);//释放解码帧对象引用
}
// 封装Bitmap图片
var bitmap = new Bitmap(width, height, dstLinesize[0], PixelFormat.Format24bppRgb, convertedFrameBufferPtr);
// 回调
show(bitmap);
//bitmap.Save(AppDomain.CurrentDomain.BaseDirectory + "\\264\\frame.buffer." + frameNumber + ".jpg", ImageFormat.Jpeg);
frameNumber++;
}
//播放完置空播放图片
show(null);
#endregion
#region 释放资源
Marshal.FreeHGlobal(convertedFrameBufferPtr);
ffmpeg.av_free(pConvertedFrame);
ffmpeg.sws_freeContext(pConvertContext);
ffmpeg.av_free(pDecodedFrame);
ffmpeg.avcodec_close(pCodecContext);
ffmpeg.avformat_close_input(&pFormatContext);
#endregion
}
/// <summary>
/// 获取ffmpeg错误信息
/// </summary>
/// <param name="error"></param>
/// <returns></returns>
private static unsafe string GetErrorMessage(int error)
{
var bufferSize = 1024;
var buffer = stackalloc byte[bufferSize];
ffmpeg.av_strerror(error, buffer, (ulong)bufferSize);
var message = Marshal.PtrToStringAnsi((IntPtr)buffer);
return message;
}
public void Stop()
{
CanRun = false;
}
}
}
打印机使用微软自带的 PrintDocument
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;
namespace FloorScale.WinForm.Common
{
public class PrintService
{
public PrintService()
{
//
// TODO: 在此处添加构造函数逻辑
//
this.docToPrint.PrintPage += new PrintPageEventHandler(docToPrint_PrintPage);
}//将事件处理函数添加到PrintDocument的PrintPage中
// Declare the PrintDocument object.
// 创建一个PrintDocument的实例
private System.Drawing.Printing.PrintDocument docToPrint = new System.Drawing.Printing.PrintDocument();
private string streamType;
private string streamtxt;
private Image streamima;
// This method will set properties on the PrintDialog object and
// then display the dialog.
public void StartPrint(string txt, string streamType)
{
this.streamType = streamType;
this.streamtxt = txt;
// Allow the user to choose the page range he or she would
// like to print.
// 创建一个PrintDialog的实例。
System.Windows.Forms.PrintDialog PrintDialog1 = new PrintDialog();
PrintDialog1.AllowSomePages = true;
// Show the help button.
PrintDialog1.ShowHelp = true;
if (!string.IsNullOrWhiteSpace(FrmMain.GlobalConfig.PrintMin.Name))
docToPrint.PrinterSettings.PrinterName = FrmMain.GlobalConfig.PrintMin.Name;
// Set the Document property to the PrintDocument for
// which the PrintPage Event has been handled. To display the
// dialog, either this property or the PrinterSettings property
// must be set
PrintDialog1.Document = docToPrint;//把PrintDialog的Document属性设为上面配置好的PrintDocument的实例
// 调用PrintDialog的ShowDialog函数显示打印对话框,如果不要注释即可,直接调用docToPrint.Print()
//DialogResult result = PrintDialog1.ShowDialog();
// If the result is OK then print the document.
//if (result == DialogResult.OK)
//{
// docToPrint.Print();//开始打印
//}
docToPrint.Print();//开始打印
}
public void StartPrint(Image ima, string streamType)
{
this.streamType = streamType;
this.streamima = ima;
// Allow the user to choose the page range he or she would
// like to print.
System.Windows.Forms.PrintDialog PrintDialog1 = new PrintDialog();//创建一个PrintDialog的实例。
PrintDialog1.AllowSomePages = true;
// Show the help button.
PrintDialog1.ShowHelp = true;
PrintDialog1.Document = docToPrint;//把PrintDialog的Document属性设为上面配置好的PrintDocument的实例
DialogResult result = PrintDialog1.ShowDialog();//调用PrintDialog的ShowDialog函数显示打印对话框,如果不要注释即可,直接调用docToPrint.Print()
// If the result is OK then print the document.
if (result == DialogResult.OK)
{
docToPrint.Print();//开始打印
}
//docToPrint.Print();//开始打印
}
// The PrintDialog will print the document
// by handling the document's PrintPage event.
private void docToPrint_PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs e)//设置打印机开始打印的事件处理函数
{
// Insert code to render the page here.
// This code will be called when the control is drawn.
// The following code will render a simple
// message on the printed document
switch (this.streamType)
{
case "txt":
string text = null;
System.Drawing.Font printFont = new System.Drawing.Font
("Arial", 7, System.Drawing.FontStyle.Regular);//在这里设置打印字体以及大小
// Draw the content.
text = streamtxt;
//e.Graphics.DrawString(text, printFont, System.Drawing.Brushes.Black, e.MarginBounds.X, e.MarginBounds.Y);
e.Graphics.DrawString(text, printFont, System.Drawing.Brushes.Black, 0, 10);//设置打印初始位置
break;
case "image":
System.Drawing.Image image = streamima;
int x = e.MarginBounds.X;
int y = e.MarginBounds.Y;
int width = image.Width;
int height = image.Height;
if ((width / e.MarginBounds.Width) > (height / e.MarginBounds.Height))
{
width = e.MarginBounds.Width;
height = image.Height * e.MarginBounds.Width / image.Width;
}
else
{
height = e.MarginBounds.Height;
width = image.Width * e.MarginBounds.Height / image.Height;
}
System.Drawing.Rectangle destRect = new System.Drawing.Rectangle(x, y, width, height);
e.Graphics.DrawImage(image, destRect, 0, 0, image.Width, image.Height, System.Drawing.GraphicsUnit.Pixel);
break;
default:
break;
}
}
}
}
设备太多了,不列举了。
对接地磅设备的,厂家提供dll,就按示例来调用,相对来说比较简单。
总结
客户端负责采集数据,对稳定性有要求。还要多测试测试。
前端:https://dibang.caishiben.com
接口服务:https://scaleapi.caishiben.com
接口文档:https://scaleapi.caishiben.com/swagger/index.html