EP02 · 讲义

Genesis2000 Python 脚本实战 · 第2集 InCAM 类——看懂通信链路

本集学什么

先说明这集怎么学。即使你从没写过 Python 也能听懂——每个概念先讲「为什么」,再看「代码怎么写」。 学完这一集,你就拥有了一张通信链路的地图:命令怎么拼、怎么发、应答怎么读、大查询怎么走。这张地图,后面每一集都会用到。

回顾:第 1 集埋下的钩子

Do.isjobopened('job')
上一集结尾,我们看过一行调用背后的三层接力:方法拼命令,底层发命令,Genesis 回应答。 这一集,我们把这三层掰开揉碎,一层一层看它的真实源码。看完你就明白:封装不是魔法,是老老实实的代码。

背景:两个进程一根管道

脚本进程 py3   ⇄ 标准输出 / 标准输入 ⇄   Genesis 主进程
先讲背景。你的脚本和 Genesis 主程序,是两个独立的进程,靠两条管道相连:脚本的标准输出是命令出去的通道,标准输入是应答回来的通道。 这正是「在线命令」的含义:脚本活着,命令一条条实时发出去,应答一条条实时收回来。整个 py3_package 的地基就建立在这个机制上。

行命令体系:Genesis 的自动化语言

open_job,job=xxx  # 行命令
管道里跑的语言,叫行命令。Genesis 图形界面里几乎每一个操作,背后都对应一条行命令——打开料号、导出资料、创建层别,都有。 官方还配套了脚本录制、脚本调试器和 hooks 钩子机制。行命令是协议层的公共语言,任何语言都能说。

InCAM 类定位

class InCAM:
现在看主角。IncamClasses 文件里的 InCAM 类,是全包最底层:上层所有方法,最终都汇到这里,由它负责和 Genesis 主进程对话。 文件只有三百五十四行,结构清晰。我们按实例化、发送、应答、查询四条线把它读透。

实例化第一样:协议前缀

self.prefix = '@%#%@'
实例化时准备了什么?第一样,协议前缀——一串特殊字符。它的作用是标记:接下来写出的这一行,是发给 Genesis 的命令。 为什么需要标记?因为脚本自己也往标准输出打印信息,比如调试日志。有了前缀,两边才能分清:带前缀的行是命令,不带的是脚本自己的输出。

第二样:五个应答槽位

self.STATUS = None   # 状态码
self.READANS = None  # 应答文本
self.COMANS = None   # COM 应答
self.PAUSANS = None  # 暂停应答
self.MOUSEANS = None  # 鼠标应答
第二样,五个应答槽位:状态码、应答文本、COM 应答、暂停应答、鼠标应答,全部初始化为空。 每次通信的返回结果,都装进对应的槽位。槽位先清空再使用——这个细节后面会专门讲,它是防串话的关键。

第三样:唯一临时文件名

tmp = 'incam_' + str(pid) + '.' + str(time.time())
第三样,生成一个唯一的临时文件名:进程号拼上当前时间。 这样即使十几个脚本同时运行,临时文件也绝不会重名。这个文件后面有大用——查询结果就写在这里。脚本正常结束时,析构函数自动清理它,不留垃圾。

sendCmd:拼帧与发送(完整源码)

def sendCmd(self, cmd, args=''):
  self.blank()                            # ① 清空应答槽
  wsp = ' ' * (len(args) > 0)           # ② 有参数才补空格
  cmd = self.prefix + cmd + wsp + args + '\n' # ③ 拼帧
  sys.stdout.write(cmd)                # ④ 写出
  sys.stdout.flush()                  # ⑤ 立即刷新
发命令的核心是 sendCmd,我们逐行看。第一步先调 blank,把五个应答槽位全部清空——防止上一次的旧应答残留,污染这一次的结果。 第二步到第四步拼帧:前缀、命令名、参数用空格隔开,末尾加换行。注意只有带了参数才补空格,没参数就紧挨着。 最后写进标准输出,并立即刷新缓冲区——命令必须马上送出,不能躺在缓冲区里等。

COM:读取两行应答(完整源码)

def COM(self, args):
  self.sendCmd('COM', args)
  self.STATUS = int(input())   # 第一行:状态码
  self.READANS = input()        # 第二行:应答文本
  self.COMANS = self.READANS[:]
  return self.STATUS
命令发出去之后就是等应答。COM 方法从标准输入连读两行:第一行状态码转成整数,第二行应答文本。 状态码是整个协议的关键——0 代表成功,非零代表出错。它作为返回值一层层传上去,上层方法靠它判断成败。 应答文本还会拷贝一份到 COMANS 属性,脚本想拿原始返回内容时从这里读。

完整往返:isjobopened 的一生

Do.isjobopened(job)
  → sendCmd('COM', 'is_job_open,job=xxx')
  → STATUS=0, READANS='yes'
  → 判断 == 'yes' → 返回 True
把发送和应答拼起来,看一条命令的完整生命周期:调用 isjobopened,底层拼出 is_job_open 命令发出去,读回状态码和应答文本。 应答是 yes 就返回真,否则返回假。方法本身只有四行——理解了 sendCmd 和 COM,整个包的调用在原理上就没有秘密了。

AUX 与编辑器组号

AUX set_group=<组号>   # 把后续命令定向到指定编辑器
除了 COM,还有同格式的 AUX 命令。典型用途是编辑器定向:Genesis 可以同时打开多个编辑器窗口,每个窗口一个组号。 打开实体时命令会返回组号,后续操作先发 set_group 把命令对准指定编辑器——官方文档明确写着,组号对脚本至关重要。

PAUSE 与 MOUSE:和操作员配合

自动化不等于无人化。PAUSE 发一条提示后暂停脚本,等操作员在 Genesis 里完成操作再继续;MOUSE 让操作员点一个位置,脚本拿到坐标接着算。 注意它们的应答读三行——比 COM 多一行交互应答,单独存进 PAUSANS 或 MOUSEANS。命令类型不同,读取方式也不同。

开关四件套

还有四个开关命令。SU_ON 和 SU_OFF 控制特权模式——某些操作需要更高权限才能执行。 VON 和 VOF 是错误跳过开关:VOF 之后,命令出错脚本也不会停下来,程序继续往下跑——在需要抓取错误码、或允许部分命令出错继续执行的场景里,都会用到这一对开关。

info 查询为什么走文件

COM info,out_file=<临时文件>,write_mode=replace,args=...
  → 结果整个写进临时文件
  → 脚本逐行读回
两行应答适合短结果,但查询数据可能很大——层别清单、矩阵信息,几十上百行,走应答通道太勉强。 做法是命令里指定输出文件,把结果整个写进临时文件,脚本再逐行读回。write_mode 是 replace,每次覆盖。这是官方 info 命令的标准用法。

INFO 完整源码

def INFO(self, args):
  self.COM('info,out_file=%s,write_mode=replace,args=%s' % (self.tmpfile, args))
  try:
    lineList = open(self.tmpfile, 'r', encoding='utf-8').readlines()
  except:
    lineList = open(self.tmpfile, 'r', encoding='gbk', errors='ignore').readlines()
  os.unlink(self.tmpfile)
  return lineList
看完整源码。先发 info 命令把结果写进临时文件,然后读回所有行,最后删掉临时文件。 细节在读取:先按 UTF-8 解码,失败再按 GBK 兜底——中文环境下的编码问题绕不开,这个双保险是踩坑踩出来的。 DO_INFO 和 GFDO_INFO 在此基础上多一步:把读回的行交给解析器,整理成字典再返回。

解析器家族

文件里有四个解析器,针对不同查询结果:parseInfo 把值转成数字;AskParseInfo 用 eval 解析结构;gfparseInfo 保留字符串;显示类输出有专门的解析器。 最常用的是 gfparseInfo:逐行按等号切成键和值;值带括号的切成列表,不带括号的取字符串。上层要料号列表时,就取字典里 gJOBS_LIST 这样的键。

一个 bug 的故事

parseInfo 里有个值得讲的 bug:判断类型时拿类型对象和字符串比较,这个条件永远不成立,导致多值字段解析不出来。 影响是:上层查询基本都改走 GFDO_INFO 这条字符串化通道了。一个 bug 反过来塑造了后续的架构选择——读老代码时,这类坑要心里有数。

dbutil 旁路(完整源码)

def dbutil(self, *args):
  if os.environ.get('INCAM_PRODUCT'):
    binary = os.path.join('/incam/release/bin', 'dbutil')
  else:
    binary = os.path.join(self.edir, 'misc', 'dbutil')
  fd = os.popen(binary + ' ' + ' '.join(args))
  return fd.readlines()
还有一条不经过协议的旁路:dbutil。它不写标准输出,而是直接用系统调用执行外部程序,读取返回行。 它按环境自动选路径:InCAM 环境用 release 目录下的版本,Genesis 环境用安装目录下的版本。料号列表、锁测试这类数据库操作走这条路。

使用案例巡览

Do.do.GFDO_INFO("-t root -d JOBS_LIST")  # 查料号清单
Do.do.dbutil('list jobs *')             # 列出资料库料号
Do.get_job_lock_list()                   # 查锁定列表
最后看真实用法。这三个例子摘自包内文档的「使用例子」:查料号清单走 GFDO_INFO,列资料库料号走 dbutil,查锁定列表用封装好的方法。 第 3 集起,我们会在上层反复遇见它们。这一集的通信地图,就是为后面铺路。

工程细节里的故事

最后看几处注释里的工程故事。取安装目录的代码旁写着:自动任务时无法获取环境变量,改为绝对路径——自动化环境变量缺失是真实踩过的坑。 读取结果先 UTF-8 后 GBK,编码问题在中文环境绕不开;析构函数清理临时文件,避免任务跑久留下一地垃圾。这些细节,都是真刀真枪用出来的经验。

小结与预告

这一集,我们把 InCAM 类彻底看透了:命令怎么拼、怎么发、应答怎么读、大查询怎么走文件、旁路怎么绕。 下一集往上走一层,看 MainInfo 怎么利用这些 info 命令,把料号、步骤、层别的查询封装成好用的接口——第 3 集见。
零壹AI工作室出品 · 《用 Python 驱动 Genesis2000》免费公开课 · www.binwindai.com