简介这是面向TIPTOP ERP用户及C#开发者的电子看板项目以“MFGWhiteBoard”为解决方案用于在生产现场实时展示任务信息并伴随语音提示解决ERP系统数据反馈不及时、车间看板搭建难的问题。压缩包约2.37MB内含96个文件以31个C#源码为核心辅以8个DLL动态库、6个可执行程序、5个配置文件及XAML界面设计文件项目按DAL、UI、Model等模块组织结构清晰便于定位与改造。资源附带详尽的项目说明文本逐一写明需下载的组件SDK、连接字符串替换位置与运行注意事项即使对看板开发不熟悉的读者也可按步骤完成部署其中还包含可直接编译的解决方案与界面图片能够快速预览显示效果。目前已有1524人在线学习适合想快速为自己的TIPTOP系统增加电子看板、提升现场管理档次的实施或研发人员。1. C# 电子看板把 TIPTOP 数据变成车间里看得见的现场指令C# TIPTOP电子看板本质上是把TT系统里的生产数据按产线实时拉出来渲染成车间大屏上的一目了然的生产指令。做这行的都见过这种场景车间办公室墙上贴着Excel打印的进度表早上更新一次下午就过时了计划员被产线问得最多的就是我这单到底什么时候上线。电子看板解决的就是这个信息差问题——工单达成率、设备状态、缺料预警、不良数全在屏幕上滚不用再追着人问。适合正在用TT、又有车间现场可视化需求的制造企业尤其计划、制造、品质三个部门天天对进度拉扯的团队。我最早接触是在某家电子代工厂做产线升级那个项目让我把这块的技术套路摸了个遍后面几个项目基本是复制粘贴再改改。2. TIPTOP 数据层打通直连、中间表还是服务接口——先想清楚再动手2.1 三种取数方式的真实取舍拿到需求先别急着写界面先把数据从TT里运出来的路想好。我接触过的TT环境底层数据库多为Oracle或Informix这类重型数据库不同版本开放的访问方式差别很大。常见的有三条路直连TT的数据库读视图或表。这条路最直接实时性最好C#里写好SQL查就行。缺点也明显TT版本升级或打补丁时表结构一改看板就黑屏并且直接高频查询会对ERP生产库产生压力。某次我为了刷新快把轮询间隔设成3秒结果TT那边DBA找过来说数据库会话数暴增那次之后我再不敢对生产库乱来。调用TT自身的服务接口或Web服务。这条路最正统权限和频率都有管控但麻烦在于接口文档不全不同版本接口名称、入参规则差异很大实施周期长而且很多企业的TT实施方不在场根本拿不到接口清单。中间表/中间库。这是我最推荐的方式TT侧通过作业或触发器把需要展示的数据定期写到一张中间表C#看板服务只读这张中间表不碰TT的业务表。折中的实时性完全够用——车间看板5秒到1分钟的延迟没人会介意换来的是TT侧绝对安全两边互不干扰。TT挂了中间表还在看板还能显示最后一次快照。2.2 一份能落地的中间表设计中间表的设计决定了后面所有C#代码的复杂度。以最常见的产线工单达成率看板为例我会在TT侧的中间库建一张类似这样的表CREATE TABLE TT_WIP_BOARD ( BOARD_ID VARCHAR2(20) NOT NULL, -- 看板编号对应某个车间工位 ORDER_NO VARCHAR2(30) NOT NULL, -- 工单号 PRODUCT_NAME VARCHAR2(100), -- 产品名称 PLAN_QTY NUMBER(10,2), -- 计划数量 DONE_QTY NUMBER(10,2), -- 完工数量 NG_QTY NUMBER(10,2), -- 不良数量 LINE_STATUS VARCHAR2(10), -- 产线状态 RUN/STOP UPDATE_TIME DATE, -- 数据更新时间 PRIMARY KEY (BOARD_ID, ORDER_NO) );这张表的每个字段都有讲究。BOARD_ID是看板端区分数据归属的钥匙一条产线对应一个ID避免看板显示串线UPDATE_TIME是增量同步的基石C#端靠它判断哪些是新增或修改的行LINE_STATUS这种状态码字段我用过没有的版本结果看板只能显示数字产线是否停线要现场喊话才知道后来加上了联动报警就简单了。TT侧写这张表常见做法是利用TT自带的定时作业每分钟跑一次汇总SQL往中间表刷数据。这里的频率要综合考虑太频繁TT压力大太慢看板显得不实时。我一般建议30秒到60秒一次。C#端轮询中间表的间隔可以设成5~10秒这个梯度延迟在车间完全无感。2.3 连接串、超时与连接池参数C#读Oracle我常用ODP.NET连接串里几个参数踩坑后固定下来了connectionStrings add nameTTBoard connectionStringData Source(DESCRIPTION(ADDRESS(PROTOCOLTCP)(HOST192.168.1.10)(PORT1521))(CONNECT_DATA(SERVICE_NAMETTDB)));User Idboard_read;Passwordxxxx;Poolingtrue;Min Pool Size2;Max Pool Size10;Connection Timeout15; providerNameOracle.ManagedDataAccess.Client / /connectionStrings注意几个参数Poolingtrue必须开轮询模式下频繁开连接会拖垮TT库端Min Pool Size2保证连接池里至少两条连接常驻避免偶发性的首次连接慢Connection Timeout15设短一点TT端异常时看板侧能快速失败而不是无限挂起Max Pool Size10已经够用单条连接串并发超过10个会话通常是代码设计有问题。权限方面强烈建议给C#单独建一个board_read账号只授中间表的SELECT权限。我见过有项目直接用TT运维账号连库一旦代码有SQL注入点或者误操作后果不堪设想。最小权限原则在ERP集成里不是技术洁癖是保命底线。3. C# 取数与增量同步轮询间隔、断线补偿和并发保护3.1 取数引擎主循环中间表就位之后C#侧要做的是一个常驻的取数服务。最开始我图省事直接用System.Windows.Forms.Timer塞在UI线程里结果窗口一拖动就卡数据刷新也延迟。后来规规矩矩写成后台服务模式核心主循环长这样private bool _running; private DateTime _lastSyncTime DateTime.MinValue; // 上次增量时间戳 public async Task RunAsync(CancellationToken ct) { _running true; while (_running) { try { var newRows await FetchIncrementalAsync(_lastSyncTime); if (newRows.Any()) { BoardDataCache.Instance.Merge(newRows); _lastSyncTime newRows.Max(r r.UpdateTime); } else { // 没有新数据时每分钟做一次轻量心跳 BoardDataCache.Instance.Touch(); } } catch (Exception ex) { LogHelper.WriteError(TT轮询异常, ex); } await Task.Delay(TimeSpan.FromSeconds(5), ct); // 轮询间隔5秒 } }这段代码的关键是维护一个_lastSyncTime递增游标每次按UPDATE_TIME _lastSyncTime查询取到数据后更新游标。注意Max(r r.UpdateTime)这步不能省可以用当前时间推挤但直接用最大时间更稳——下一轮不会漏掉边界行的更新。轮询间隔5秒是基于第2章中间表30秒刷新频率定的形成TT侧30秒算一次 → 看板5秒查一次 → 画面看延迟最多35秒的链路车间反应很自然。如果是设备状态看板要求更高频可以把频率调成2秒但前提是中间表写入端扛得住且查询走UPDATE_TIME索引。3.2 断线补偿与时间戳策略取数服务不可能永远在线。车间断电、网络抖动、TT库维护这些情况发生了之后看板重新连上时不能丢数据也不能重复跑全量。我维护一个本地小文件存上次成功同步的时间戳启动时先读出来再跟TT侧最大UPDATE_TIME对比决定走增量还是全量。伪逻辑如下private DateTime GetRecoveryPoint() { // 持久化时间戳程序崩溃重启后不丢游标 if (File.Exists(sync_point.txt)) { return DateTime.Parse(File.ReadAllText(sync_point.txt)); } return DateTime.Now.AddHours(-1); // 无记录时往回拉1小时宁可多读不可漏读 } private void PersistPoint(DateTime point) { File.WriteAllText(sync_point.txt, point.ToString(yyyy-MM-dd HH:mm:ss)); }断线时间不超过中间表保留窗口时这个策略能平滑补偿断线时间太长比如超过一天中间表里的历史数据可能已被清理这时要回退到全量同步并在界面上给一个明显的数据恢复中标识。我踩过这个坑某车间放假三天中间表保留两天数据看板恢复时静默缺失第一天数据产线主管盯着屏幕跟我说这个数不对那场面得靠全量同步参数才能化解。3.3 并发写保护与日志埋点取数服务把数据写入内存缓存时同时会有多个看板界面线程在读取。如果直接用ListT一边写一边读抛异常是迟早的事。public class BoardDataCache { private static readonly LazyBoardDataCache _instance new LazyBoardDataCache(() new BoardDataCache()); public static BoardDataCache Instance _instance.Value; private ConcurrentDictionarystring, WipOrder _orders new ConcurrentDictionarystring, WipOrder(); public void Merge(IEnumerableWipOrder newRows) { foreach (var row in newRows) { _orders[row.OrderNo] row; // 按工单号覆盖写 } } public IReadOnlyListWipOrder Snapshot() { return _orders.Values.ToArray(); // 放回数组快照,避免调用方长期持有引用 } }ConcurrentDictionarystring, WipOrder的覆盖写是原子操作不会出现读取时一半新一半旧的混乱状态Snapshot()返回ToArray()快照是确保UI循环遍历时集合不会被并发修改撑爆。服务刚启动全量同步时_orders可能会有几千条数据用快照方式拿引用能让UI线程完全不感知后台在Merge。日志埋点在排查问题时是后悔药。每个轮询周期写一行精简日志就够了时间、拉取行数、耗时、当前缓存总量。用文件日志按天切割保留两周。后来TT侧有时候数据没更新DBA一口咬定我们代码有问题翻出日志给他看中间表在哪个时间段确实没有新写入一次就定位到TT作业挂了。这个习惯让我少吵很多架。4. 看板界面与渲染调度把数据画在车间大屏上的正确姿势4.1 WPF 布局先决定横屏还是竖屏车间看板呈现形态五花八门常见的是21:9宽屏挂在产线正上方也有竖屏立在过道口。要在一开始就让UI设计遵循信息分层原则最上面1/4是产线级汇总中间一半是工单明细表格最下面1/4是异常滚动条。WPF里我建议用Grid做整体骨架不要用StackPanel硬排因为Grid的*号按比例缩放能让看板在不同分辨率下保持合理布局。Grid Grid.RowDefinitions RowDefinition Height2*/ !-- 汇总区占上部空间 -- RowDefinition Height5*/ !-- 工单明细区占主要空间 -- RowDefinition Height1*/ !-- 异常滚动区 -- /Grid.RowDefinitions Grid Grid.Row0 !-- 汇总卡片区域 -- /Grid DataGrid Grid.Row1 x:NameDgOrders ... / ListBox Grid.Row2 x:NameLbAlerts ... / /Grid字号适配是个玄学问题。22寸屏和55寸大屏字号需求完全不同我采用Viewbox包住整个根容器让WPF自动缩放。果断放弃固定字号方案不然每个车间都要单独调UI维护成本收不住。4.2 数据绑定与 Dispatcher 调度MVVM模式下看板ViewModel暴露属性后台取数服务把数据推进缓存后界面怎么高效刷新我踩过UI线程阻塞的坑根源是直接在事件回调里跑SQL。正确做法是取数全部在后台线程完成UI用一个DispatcherTimer定时从缓存拿快照再整体刷新绑定。public class BoardViewModel : INotifyPropertyChanged { private DispatcherTimer _refreshTimer; private ObservableCollectionWipOrderView _orderList; public BoardViewModel() { _refreshTimer new DispatcherTimer(); _refreshTimer.Interval TimeSpan.FromSeconds(5); _refreshTimer.Tick OnRefreshTick; _refreshTimer.Start(); } private void OnRefreshTick(object sender, EventArgs e) { // 从缓存拿快照再转成UI视图模型 var snapshot BoardDataCache.Instance.Snapshot(); var viewModels snapshot.Select(s new WipOrderView(s)).ToList(); // UI线程操作集合 _orderList.Clear(); foreach (var vm in viewModels) { _orderList.Add(vm); } OnPropertyChanged(nameof(OrderList)); } }这里的几个细节都是现场逼出来的_refreshTimer跑在UI线程回调里绝对不能做数据库查询只从内存缓存拿数据ObservableCollection操作前一定要先Clear()再逐个Add()不要用复杂的增量对比逻辑5秒刷新一次全量几千行的列表在现代工控机上毫无压力WipOrderView在构造函数里做好格式化不要在XAML绑定里做字符串拼接绑定表达式一复杂界面上偶发出现格式错误的红色标记很难查。4.3 多屏与全屏的细节车间的显示器往往常年通电DPMS休眠一定要关。我部署时会顺手把Windows电源计划改成高性能并关掉屏幕关闭超时。这个细节决定了看板会不会半夜自动熄屏早上来一片黑。全屏窗口显示优先级要最高工控机偶尔会弹Windows更新提示直接挡住看板数据我是用一个全局钩子禁用这类弹窗的顺带把系统主题切成经典模式避免偶发的GPU驱动渲染问题。一台工控机带两块屏幕的情况WPF默认只在一个窗口内显示我处理的方案是一个进程内开两个Window各自绑定不同BOARD_ID的数据。别贪图省事搞成两个进程连同一个中间表那等于把第3章的并发问题又引进来了。5. 避坑 / 常见问题从车间现场带回来的排查记录5.1 现象看板显示的数据比ERP慢15分钟某天车间主管指着屏幕说看板上这个工单早就完工了怎么还显示生产中。我第一时间查TT作业日志发现中间表确实没有新数据。再翻TT侧的定时作业配置原来设置的是每小时整点跑一次不是我以为的30秒一次。原因TT侧的定时作业频率是实施人员手配的跟看板的轮询频率完全脱节。解决把TT作业改成每30秒执行一次并在中间表加一个UPDATE_TIME的默认值做兜底同时在C#端日志里记录每次拉到的最大时间戳如果看板数据超过2分钟没变化界面显示黄色数据延迟角标提醒现场人员。数据准不准和数据新不快在车间是两件事前者靠比对后者靠监控缺一不可。5.2 现象看板画面卡死鼠标能动但数据不动工控机上鼠标还能动说明Windows没崩但看板界面看起像死机。打开任务管理器发现CPU占用不高但是UI线程被占死了。原因我在初期版本里直接在CollectionChanged事件里做了耗时计算一有集合变化就触发UI重算数据量一大就把UI线程拖住了。后来把所有计算全部转移到后台线程UI只做简单绑定问题消失。要排查这类问题Visual Studio里用调试→窗口→线程看一眼UI线程有没有被阻塞以及在DispatcherTimer回调入口加一个Debug.WriteLine是定位最快的方式。5.3 现象换一台老工控机就白屏同一个看板程序在开发机好好的部署到一台老旧的Win7工控机上启动后整个窗口白屏偶尔闪一下又白。原因WPF默认启用硬件加速老显卡驱动对部分渲染特性支持不全导致画面渲染失败。解决在App.xaml.cs启动时加一行禁用硬件加速的代码RenderOptions.ProcessRenderMode RenderMode.SoftwareOnly;强制软件渲染帧率在静态看板场景下完全够用。同时字体别用微软雅黑以外的花哨字体老系统字体渲染引擎会出毛边。这条经验让我后面看板程序都默认软件渲染省得在不同工控机间来回折腾。提示软件渲染在超高清大屏上字体边缘会有轻微锯齿可在观感与兼容性之间取舍。实际部署中优先保证稳定画面质感可以通过配色和排版弥补。5.4 现象同一条产线两台看板数据不一致一条产线头尾各挂一块屏显示的内容居然不一样生产主管拍了照发群里问是不是系统出故障了。排查发现两台看板各自独立建了连接轮询时间点不同而中间表恰好在这期间被TT侧更新导致一个读到旧快照一个读到新快照。原因多客户端直连同一数据源没有统一的版本协调。解决部署一台取数服务作为唯一数据入口看板客户端不直连数据库而是从取数服务的内存缓存订阅数据。可以用一个轻量级的Ipc或者共享文件做中转车间规模不大时最简单的是让所有看板连接同一个WebSocket服务数据由服务端统一广播。从那以后所有多屏项目我强制走单入口广播模式再没出现过屏间数据打架。5.5 现象重启取数服务后数据大跳变某次意外断电重启取数服务看板数据突然显示几百条工单且数量一直在跳。原因重启后走了全量同步而TT侧的中间表刚好在跨天刷新读了事务未提交的半成品数据。解决在C#端做一步过滤只认LINE_STATUS RUN的工单以及UPDATE_TIME不大于服务启动时间的行。var snapshot BoardDataCache.Instance.Snapshot() .Where(o o.LineStatus RUN) .ToList();顺带在取数服务启动时加一个Delay(5s)等TT侧的刷新作业跑完一个完整周期再开始拉数。这条算是对数据一致性的敬畏吧。6. 进阶从看得见到管得住——让看板成为生产闭环的一部分当看板稳定显示数据之后下一步自然是让数据反哺操作。我最近一个项目给看板加了两项能力把车间管理粒度从分钟级压到秒级。6.1 加一条轻量级消息通道轮询模式适合展示型看板但设备报警、缺料呼叫这种即时事件轮询5秒太慢了。我的做法是引入SignalR取数服务发现LINE_STATUSSTOP时立即推送给看板端看板弹出红色报警条并联动声音提示。取数服务变成消息发布端数据来源端真正实现了眼到、手到、响应到public class BoardHub : Hub { // 取数服务调用该方法推送报警 public async Task PushAlert(string boardId, string message) { await Clients.All.SendAsync(OnAlert, boardId, message); } }客户端订阅很简单HubConnection维护常驻连接的重连机制要设成自动车间网络偶尔抖动自动回了就恢复。加多线程后SignalR的阻尼问题没有想象复杂一条产线几十块屏规模时完全够密。6.2 异常自动弹屏与广播看板不只是显示器还能成为标准作业的强制节点。当某工单NG_QTY超过阈值看板立即弹出该工单的检验标准操作卡缺料时自动追加上一张物料临时变更单。这个玩法把看板从数据面板升级成现场督导终端。6.3 加上布控操作记录最近我在看板右下角加了一个小的异常确认按钮班组长点击后记录点击人、工号和确认时间回传到TT侧形成闭环。仅靠看板显示并不能提升管理真正有价值的是工位快速响应动作。从那以后我每一套看板系统交付前都强制走一遍断电重启看板自动恢复完整显示的演练确认数据补偿正常、报警推送正常、操作记录可查询再签字上线。车间数字化看着是技术活落地到最后一公里还是习惯与流程。希望这个拆解对你有点帮助祝你少踩几个车间现场的坑。本文还有配套的精品资源点击获取