简介:围绕WonderWare Intouch 2014R2与SQL Server 2012的数据集成及Excel报表开发展开的工程文档,面向SCADA/HMI实施人员、自动化工程师及需要完成实时数据入库与报表输出的技术人员,重点解决Intouch变量绑定、数据库连接管理与报表模板搭建中的配置衔接问题。包内为1份doc文件,压缩包约2.48MB,内容以图文步骤和脚本示例组织,便于对照现场平台版本查阅。已有533人学习下载。文档覆盖数据库登录方式与权限设置、标记名字典中连接状态和时间变量创建、条件脚本触发、SQL访问管理器绑定列表、SQLConnect与SQLDisconnect连接及关闭处理、SQLInsert写入判断,以及Excel VBA引用、报表样式模板和SQL数据库文件恢复附加等环节,可帮助读者梳理从数据采集入库到报表呈现的完整链路,并作为Intouch与SQL Server联调时的参考材料。
1. 一份 .doc 拆开看:InTouch 2014 R2 落库,卡住的是四处而不是一步
现场最常见的场面是:HMI 上曲线跑得好好的,一到月底要交班报,翻历史库发现一条记录都没有。这份 .doc 讲的就是把这条链路补上——在 InTouch 2014 R2 的 IDE 里用应用程序脚本把变量写进 SQL Server 2012,再用 Excel 的 VBA 把库里的数据拉成日报,最后把 mdf/ldf 搬到报表服务器上附加回来。它面向的是已经在做 SCADA 上位机、需要交付生产报表的人,而不是想找 ORM 方案的 Web 开发者。整份文档按“建库—连接—绑定—写入—报表—恢复”顺序编排,看着像流水账,实际卡人的只有四处:数据库验证模式、绑定列表的列对齐、条件脚本的触发时机、以及那个容易被写错位置的去重标志。下面按我自己的实施顺序重排一遍。
2. 打通 SQL Server 2012:验证模式、登录名与表结构的三层对齐
2.1 混合验证模式不是可选项,是前置条件
InTouch 的 SQL 函数底层走 OLE DB,连接串里带User ID和Password,也就是 SQL 标准登录。如果实例只开了 Windows 身份验证,连接串里就算写sa也连不上,返回的是一个非 0 的错误码,而 HMI 上不会弹任何提示——这是第一个让人怀疑人生的地方。
改法在文档里写得很简略:SSMS 里右键实例 → 属性 → 安全性 → 选“SQL Server 和 Windows 身份验证模式”。但漏了最关键的一步:改完必须重启 SQL Server 服务。这个选项本质是注册表里的LoginMode,引擎只在启动时读取一次,不重启的话 SSMS 里看着已经改了,实际连接仍然被拒。
动手之前先确认现状,别凭印象:
-- 返回 1 表示仅 Windows 验证,返回 0 表示已开启混合模式 SELECT SERVERPROPERTY('IsIntegratedSecurityOnly') AS OnlyWindowsAuth;如果返回 1,就去改并重启。改完再跑一次,返回 0 才算过关。重启生产库要排窗口,这件事在项目排期里就得提前说,别等到联调当天才发现要停库。
2.2 登录名要单建,别把 sa 写进脚本
文档示例里的连接串用的是sa,调试阶段图省事可以理解,但直接上生产有两个问题:一是sa权限过大,一旦脚本或工程文件外流,整个实例都暴露;二是密码明文躺在应用程序脚本里,任何能打开 IDE 的人按两下就能看到。
合格做法是建一个只够用的登录名,只给目标库的读写角色:
-- 1) 建 SQL 标准登录,开启密码策略 CREATE LOGIN scada_writer WITH PASSWORD = 'Chg_Me#2024', CHECK_POLICY = ON; -- 2) 建库 CREATE DATABASE YADC_BB; -- 3) 建用户并映射到登录名 USE YADC_BB; CREATE USER scada_writer FOR LOGIN scada_writer; -- 4) 只给读写,不给 ddl 权限 ALTER ROLE db_datareader ADD MEMBER scada_writer; ALTER ROLE db_datawriter ADD MEMBER scada_writer;参数上注意两点:CHECK_POLICY = ON会强制密码复杂度,别为了省事关掉,否则过几个月渗透扫描第一个报的就是它;角色只加db_datareader和db_datawriter,db_owner不要给——InTouch 侧只需要 insert 和 select,不需要建表改表。
报表端建议再单建一个只读登录名,比如report_reader,只挂db_datareader。写数和读数的账号分开,出问题时日志里一眼能看出是谁干的。
2.3 表结构与绑定列的类型对照
文档强调“变量数据类型与数据库表单列的数据类型对应”,但没给对照关系。这恰恰是SQLInsert失败率最高的地方,因为类型不匹配时整条 insert 直接失败,InTouch 只留一个错误码,不告诉你哪一列出的问题。
常用对照如下,按这个建表基本不会翻车:
| InTouch 标记类型 | 建议列类型 | 说明 |
|---|---|---|
| Memory Discrete | bit | 只存 0/1,不要用char(1) |
| Memory Integer | int | 计数、状态码 |
| Memory Real | float | 模拟量,精度够用且转换宽松 |
| Memory Message | nvarchar(n) | 必须 n 开头,长度 ≥ 标记最大长度 |
$Date+$Time | datetime | 拼成一个字符串再写 |
建表语句照这个来:
CREATE TABLE dbo.TDays ( RecTime datetime NOT NULL, -- 对应绑定的日期时间列 NodeName nvarchar(32) NOT NULL, -- 对应 Memory Message 标记 P1 float NULL, -- 对应 Memory Real RunFlag bit NULL -- 对应 Memory Discrete );两个细节值得单独说:Message类型一定要配nvarchar而不是varchar,否则中文字段进库变问号,排查半天以为是编码问题;datetime比datetime2更稳,因为老版本 OLE DB provider 对datetime2的隐式转换支持不齐,用datetime少一类玄学。
2.4 先用命令行验证通路,再回 InTouch 里折腾
这一步文档里没有,但能省掉大量来回。在装 InTouch 的那台机器上直接开命令行连一次库,能通说明网络、实例名、登录、库权限四件事全对,后面再出问题就只可能是绑定或脚本。
# -S 服务器(命名实例要写成 主机名\实例名) # -U/-P 登录名与密码,-d 目标库,-Q 执行后立即退出 sqlcmd -S HMI_SRV01 -U scada_writer -P "Chg_Me#2024" -d YADC_BB \ -Q "INSERT INTO dbo.TDays VALUES(GETDATE(),'OP_01',1.25,1); SELECT COUNT(*) FROM dbo.TDays;"这条命令跑通,等于把“数据库侧”整段验收完了。如果sqlcmd报连接超时,八成是 SQL Server 配置管理器里 TCP/IP 协议被禁用,或者命名实例没写实例名——这两条在 InTouch 里表现完全一样,都是连不上,所以必须先在这里排除掉。
3. InTouch 侧的内存标记与条件脚本:$Second==3 到底在等什么
3.1 为什么四个辅助标记都建成内存型
文档在标记名字典里建了Connid1(内存整型)、GetDateTimeS(内存消息)、NodeName(内存消息),后面脚本里又冒出个WSQLS(内存离散)。四个都选内存型不是随手,是有原因的:内存标记不挂通讯驱动,值完全由脚本掌控,PLC 断线、驱动重连都不会把它刷掉。Connid1存的是SQLConnect返回的连接号,一旦被通讯状态刷成 0,后面所有SQLInsert全部失败,而且现场看起来“通讯正常、界面正常”,极难定位。
四个标记的用途整理成表,照着建就行:
| 标记名 | 类型 | 初值 | 用途 |
|---|---|---|---|
Connid1 | Memory Integer | 0 | 保存 SQLConnect 返回的连接号 |
NodeName | Memory Message(32) | 空 | 保存 GetNodeName 取到的本机名 |
GetDateTimeS | Memory Message(32) | 空 | 保存拼好的时间字符串 |
WSQLS | Memory Discrete | 0 | 本轮是否已写入,用于去重 |
NodeName的长度按实际机器名给,别卡在 8 位——很多现场机器名是OP_01、LINE2_ST02这种,长度不够会被截断,后面字符串比较就永远不成立,脚本静默不执行。
3.2 日期时间格式:文档里那行“0、”不是凑数
原文把“在操作系统的日期和时间中更改日期和时间格式”放在第 0 步,看起来像个无关紧要的准备工作,实际上它是整条链路能不能对齐时间的基础。$Date和$Time转成字符串时受系统区域设置影响,短日期如果是dd/MM/yyyy或者带中文“年月日”,写进datetime列时服务端按mdy解析,轻则报转换错误,重则日期整个错位——12 月 1 号被解析成 1 月 12 号,这种数据进库之后做报表是灾难。
做法是把短日期改成yyyy-MM-dd,长时间改成HH:mm:ss,改完重启 InTouch 让运行时重新读区域设置。然后在脚本里拼时间:
{拼成 SQL Server 能稳定识别的格式,不依赖区域设置} GetDateTimeS = StringFromTime($Date, 1) + " " + StringFromTime($Time, 3);StringFromTime的第二个参数是格式枚举,不同版本编号略有差异,以 IDE 内帮助里那张表为准,别照抄别人的数字。我一般会在诊断画面上把这个字符串直接显示出来,肉眼确认是2024-06-01 14:58:03这种形态再往下走。
3.3 $Second==3 与 $Second==5 的选法
两个条件脚本分别挂在系统秒等于 3 和等于 5。选非 0 的值有讲究:整点、整分那一秒系统里一堆脚本同时在跑,避开 0 能减少瞬时压力;两个脚本错开 2 秒,是为了给SQLConnect留出返回连接号的时间,否则SQLInsert可能拿着Connid1 = 0去执行。
但$Second == 3这种写法本身有个隐患:如果运行时负载高,某一秒被跳过,这个脚本当天就一次都不执行。我在一个点位上遇到过,机器 CPU 长期 80% 以上,一个月里漏了十几天,报表出现规律性空洞,查了两周才定位到是跳秒。
比较稳的写法是改成时间窗,再配去重标志:
{条件:$Second >= 3 AND $Second <= 5 AND WSQLS == 0} {含义:3 到 5 秒之间任意一次机会都能触发,WSQLS 保证只执行一次}三个参数的作用分别是:$Second是系统秒标记,每秒刷新;时间窗给三次机会对抗跳秒;WSQLS == 0防止窗口内重复执行。改完之后即使跳掉一秒,还有两次机会兜底。
3.4 应用程序脚本的三段式:连接、写入、断开
连接放在应用程序脚本的“运行时”里,程序一启动就建立:
{应用程序脚本 - 运行时} {provider 用 sqloledb,Data Source 换成实际机器名或 主机名\实例名} SQLConnect(Connid1, "provider=sqloledb;Data Source=HMI_SRV01;Initial Catalog=YADC_BB;" + "User ID=scada_writer;Password=Chg_Me#2024");写入脚本挂在条件$Second == 5上,逻辑比文档里的版本多了个ELSE:
{条件脚本:$Second == 5,条件类型:为真时} GetNodeName(NodeName, 131); IF NodeName == "OP_01" THEN IF $Minute >= 58 THEN IF WSQLS == 0 THEN SQLInsert(Connid1, "TDays", "TDays"); WSQLS = 1; ENDIF; ELSE WSQLS = 0; {关键:复位必须放在分钟数不足的分支里} ENDIF; ENDIF;GetNodeName的第二个参数 131 是取值模式,表示取本机计算机名,这个值在 IDE 帮助里能查到。整段的作用是:只有本机是OP_01时才写库,避免多台 HMI 同时往一张表里灌数据。
ELSE分支那行WSQLS = 0是整段脚本的命门。文档里代码行⑥和⑦是连着写的,置位后立刻复位,等于去重完全失效——58 分那一分钟里每个扫描周期都会插一条,一分钟能灌进去几十条重复记录。正确时序是这样:
| 时刻 | $Minute | WSQLS | 实际动作 |
|---|---|---|---|
| 09:57:05 | 57 | 0 | 走 ELSE,复位为 0 |
| 09:58:05 | 58 | 0 | 执行 SQLInsert,置 1 |
| 09:59:05 | 59 | 1 | 分钟条件成立但 WSQLS=1,跳过 |
| 10:00:05 | 0 | 1 → 0 | 走 ELSE 复位,等待下一个 58 分 |
这样每小时正好一条。退出时别忘了在“关闭时”脚本里断开,否则连接句柄留在服务端,累积几天会话数就打满了:
{应用程序脚本 - 关闭时} SQLDisconnect(Connid1);4. SQLConnect 连接串与绑定列表:五个字段里藏着三个翻车点
4.1 连接串逐项拆解
SQLConnect(连接号, 连接字符串)的第二个参数由五段组成,段间用分号隔开,每段键=值:
| 字段 | 文档示例值 | 实际怎么填 | 易错点 |
|---|---|---|---|
| Provider | sqloledb | 保持默认 | 别换成新版驱动,InTouch 未必加载得了 |
| Data Source | 计算机名 | HMI_SRV01或HMI_SRV01\SQLEXPRESS | 命名实例不写实例名必失败 |
| Initial Catalog | 数据库名 | YADC_BB | 填库名不是表名 |
| User ID | sa | scada_writer | 与 2.2 建的登录名一致 |
| Password | 明文 | 强密码 | 密码含分号会截断连接串 |
第一段Provider=sqloledb不要“顺手升级”。这个 provider 是随系统组件走的,InTouch 2014 R2 年代的运行时就认它;换成MSOLEDBSQL之类的新驱动,轻则连接失败,重则 IDE 加载时报错,回退起来还得重装组件。
第三段Initial Catalog填错的表现很有欺骗性:连接返回成功(因为服务器和登录都对),但SQLInsert一直失败——因为默认库可能是个空库。排查时直接SELECT DB_NAME()确认当前落在哪个库上,比猜快得多。
第五段的密码里如果带分号,连接串会在分号处被提前截断,剩下的部分被当成新的键值对解析,报的错跟密码错误完全无关。这是最血泪的一条,现场改密码时随手加了个;做分隔,结果排查了一下午。省事的办法就是密码里永远不放分号。
4.2 绑定列表:变量名和列名的对不上,全在这里
绑定列表在 SQL 访问管理器里,作用是建立“InTouch 标记 ↔ 数据库表列”的映射。SQLInsert(Connid1, "TDays", "TDays")里第二个TDays是表名,第三个是绑定列表名——两者可以同名,但概念上不是一回事,很多人以为第二个参数是绑定列表名,改表名的时候改错位置。
建完之后逐行核对,按这张对照表检查:
| 绑定列名 | 标记名 | 类型 | 对应表列 |
|---|---|---|---|
RecTime | GetDateTimeS | Message | datetime |
NodeName | NodeName | Message | nvarchar(32) |
P1 | P1_Real | Real | float |
RunFlag | RunFlag | Discrete | bit |
三条硬规则:列名必须和表里一字不差(大小写不敏感,但拼写必须对);类型必须能隐式转换过去;Message 标记声明的最大长度不能超过列的nvarchar(n)长度。第三条最阴,超长时 SQL Server 侧可能截断也可能直接报错,取决于驱动版本,现场表现不一致。
4.3 返回值别丢,它是唯一的诊断入口
SQLConnect和SQLInsert都返回整型结果码,0 表示成功,非 0 是错误。文档示例里直接调用不接收返回值,出问题只能靠猜。做法很简单,把返回值落到标记上:
{把连接结果存进 Connid1,成功时是非 0 的连接号,失败时是错误码} Connid1 = SQLConnect(Connid1, "provider=sqloledb;Data Source=HMI_SRV01;Initial Catalog=YADC_BB;" + "User ID=scada_writer;Password=Chg_Me#2024"); {写入结果单独存一个整型标记,比如 WRet,方便在线看} WRet = SQLInsert(Connid1, "TDays", "TDays");注意SQLInsert返回的是执行结果码,不是插入行数,别拿它当行数用。在 HMI 上做个巴掌大的诊断画面,把Connid1和WRet显示出来,运维人员一眼就能判断是连接问题还是写入问题,省掉大量电话沟通。
4.4 写入频率与数据量的取舍
按 3.4 的时序,一小时一条,一天 24 条,一年不到九千条,这个量级对 SQL Server 来说毫无压力,表可以几年不清理。但如果现场要求的是分钟级甚至秒级存储——那就该换方案了,条件脚本 +SQLInsert的写法撑不住:每次 insert 是一次独立事务,秒级写入会把日志文件撑爆,而且 InTouch 脚本是单线程扫描,写库耗时会直接拖慢整个运行时的响应。
判断标准很直接:日增记录数超过几万条,就该考虑用历史库组件或者批量提交,而不是往条件脚本里继续堆。这份文档的定位是班报、日报级别的抽样存储,把它用在秒级趋势上是选型错误,不是配置问题。
5. 避坑与排查:五条现场记录,从“连不上”到“跑了三天突然不写”
5.1 连接层:连不上与连上了写不进
记录一:SQLConnect一直返回非 0,Connid1永远是 0。
现象是 HMI 上诊断画面里Connid1一动不动。原因通常有三个:实例只开了 Windows 验证(2.1 那条,改完没重启服务也算);命名实例在Data Source里只写了机器名;SQL Server 配置管理器里 TCP/IP 协议被禁用。解决顺序是先在 HMI 那台机器上用sqlcmd连一次,能连说明前三个原因都不成立,问题在连接串本身;连不上就按“服务是否启动 → TCP/IP 是否启用 → 验证模式 → 登录名”逐层排除。
记录二:连接成功,SQLInsert一直失败。
现象是Connid1有值、WRet非 0。原因八成在绑定列表:列名拼错一个字母、类型对不上、或者表建在了非dbo架构下而绑定里没带架构名。解决办法是在 SSMS 里执行EXEC sp_help 'TDays';把列名和类型原样抄到绑定列表里,别凭记忆敲。
记录三:连接跑了两三天突然失效,重启 InTouch 才恢复。
这是最像玄学的一条。原因是连接被服务端空闲超时或网络闪断掐掉了,但Connid1里还留着旧句柄,脚本继续拿它去 insert,一直失败。解决思路是在条件脚本里判断返回值,失败就重连:
{每小时那次写入前先探一下连接,失败则重连} IF WRet <> 0 THEN SQLDisconnect(Connid1); Connid1 = SQLConnect(Connid1, "provider=sqloledb;..."); ENDIF;更稳妥的做法是加一张心跳表,每分钟写一条,既能保活又能当监控信号——心跳断了就是链路断了,比等报表缺数据早发现好几天。
5.2 数据层:写进去了但内容不对
记录四:同一分钟写入几十条重复记录。
现象是报表里同一时刻出现十几行相同数据。原因就是 3.4 说的那个:WSQLS = 1后面紧跟WSQLS = 0,去重形同虚设。解决是把复位挪到ELSE分支里,让它在分钟数不足 58 时才归零。改完用 4.4 那张时序表逐分钟对一遍,别只看一眼觉得对就上线。
记录五:时间列全是1900-01-01,或者整体差 8 小时。
1900-01-01是 SQL Server 对空字符串和非法日期做转换时的默认值,说明传过去的时间字符串根本没被识别。原因基本锁定在区域格式(3.2)——短日期不是yyyy-MM-dd。差 8 小时则是另一回事:HMI 和服务器的时区设置不一致,或者列用了带时区语义的类型。解决是把两边时区对齐,列统一用datetime,时间字符串统一yyyy-MM-dd HH:mm:ss格式,并且在 HMI 诊断画面上把这个字符串显示出来人工核对一次。
5.3 排查顺序:从下往上,别从上往下
现场出问题,很多人的第一反应是打开 InTouch 看脚本,这是反的。正确顺序是从数据源头往上一层一层验:
- 在 HMI 机器上
sqlcmd连库,验证网络、实例、登录、权限; - 在 SSMS 里手工
INSERT一条,验证表结构和列类型; - 检查绑定列表的列名与类型映射;
- 最后才看条件脚本的触发条件和去重逻辑。
这四层里,前两层能覆盖八成问题,而且验证成本极低。跳过前两层直接改脚本,改对了是运气,改错了还会引入新问题。
注意:每次改完连接串或绑定列表,都要把 InTouch 切到运行态重新加载一次,IDE 里改完不重启运行时,脚本用的还是旧值。
6. Excel 报表取数与库文件附加:两个收尾动作的可靠做法
报表端文档讲得比较粗,只写了“配置 Excel”和“至少勾选三项引用”。实操上我建议直接用 ADO 后期绑定,不勾引用也能跑,部署到别的机器上少一堆“引用丢失”的麻烦。要勾的话,Microsoft ActiveX Data Objects 2.8 Library这一项是必须的,其余按需。
取数的核心是把时间段作为参数传进去,而不是把整表拉出来再筛:
Sub RefreshDaily() Dim conn As Object, rs As Object, sql As String Set conn = CreateObject("ADODB.Connection") ' 后期绑定,免去引用依赖 conn.Open "Provider=SQLOLEDB;Data Source=HMI_SRV01;Initial Catalog=YADC_BB;" & _ "User ID=report_reader;Password=Chg_Me#2024;" ' 用具体时间段过滤,避免全表扫描 sql = "SELECT RecTime, NodeName, P1, RunFlag FROM dbo.TDays " & _ "WHERE RecTime >= '2024-06-01 00:00:00' AND RecTime < '2024-06-02 00:00:00'" Set rs = conn.Execute(sql) Sheet1.Range("A2").ClearContents Sheet1.Range("A2").CopyFromRecordset rs ' 一次性灌入,比逐格写快两个数量级 rs.Close: conn.Close End Sub三个参数值得盯住:报表账号用只读的report_reader,不给写权限;时间段用左闭右开,避免边界那一秒重复统计;CopyFromRecordset一次灌入,几千行比逐单元格赋值快得多。日报模板里那个“参数日报”页的按钮就绑这个过程,VBA 里再补一段按日期循环,就能批量生成一整月的报表。
库文件搬运用的是附加而不是还原,前提是目标实例的版本不低于源实例。命令行版本比 SSMS 图形界面可靠,也方便写进脚本:
-- 附加前确认 mdf/ldf 已拷到目标机,且去掉了只读属性 CREATE DATABASE YADC_BB ON ( FILENAME = 'E:\DataBase\YADC_BB.mdf' ), ( FILENAME = 'E:\DataBase\YADC_BB_log.ldf' ) FOR ATTACH;两个坑:只拷 mdf 不拷 ldf,附加会直接报错,要么把两个文件一起拷,要么用sp_attach_single_file_db让 SQL Server 重建日志;文件属性是只读时附加失败,报错信息还很不直观,拷完先右键看一眼属性页。另外附加要求文件没有被其他实例占用,源库如果还在运行,得先分离或者停服务。
从那以后我每次碰这类项目,都强制自己按“命令行连库 → 手工插一条 → 绑定列表核对 → 脚本时序表”这个顺序走一遍,哪怕觉得闭着眼都能做对。这套流程救过我不止一次——有回现场赶工期跳过了第二步,结果表建在非dbo架构下,脚本调了两天才发现。希望帮到你。
本文还有配套的精品资源,点击获取