以前从没做过桌面软件开发,更没做过客户端软件。这次的这个软件,对我来说是一个全新的设计体验。
做的软件是工具软件,类似于在桌面上点击右键弹出的‘显卡控制面板’这样的东西。时间一个月,因为用户群体也是工程师(公司芯片的客户),所以,没必要做得很炫,但也不能做得太随意,毕竟客户调试芯片就够烦了,再是用一套丑陋的工具的话...至于我个人的目标,则是希望不光是做一个面向特定芯片的工具,而是能积累出一些能在以后项目中使用的具备共性的代码,毕竟升级换代的芯片都是在一个super block的基础上做的,所以其工具的代码也应该有类似的super block。
代码不是从头磊起的,有一个早期的工具可供参考,所有工具软件基本上都类似,大致就是一些ui元件,用以反映一些用户可根据需求自由设置的芯片参数,当然是已经抽象过的了,这些ui元件最好能反映参数的语义,比如限定用户可选值的采用radio button或者list box,或者如时序这样的参数就采用编辑框让用户可以在一定值的范围内输入任意数,再加上一个slider box能进行粗调,当然,既然是时序信号,有一个像示波器那样的panel能即时反映用户输入值会导致什么样的信号输出的界面元素就更好了。其实这也是pm为什么要做新tool的原因,即除了适用于新的片子外,更加user friendly,用一个好的工具,让用户可以扔掉手册。
拿过来的早期代码是用wxPython做的,谢天谢地,幸好不是mfc或者c#,后者主要是自己没学过,虽然该货是做这种软件的大杀器。wxPython做ui,很多设计我还是蛮喜欢的,比如sizer,和mfc相比,有这么一个好用的layout管理器太赞了,类似的东西如果在mfc下面得在codeproject上去找第三方的了...我还记得以前在mfc下面用对话框管理器调节控件位置的时候...wxPython的开发效率个人认为也是很高的:解释型语言,轻量级工程,大量的容器,撇开这些基础的不说,如eval,函数式编程才有的特性如closure等,都为开发带来了极强的便利性。不过,没有一个向vs那样的调试器也许是个槽点,不过长期在底层工作的我,早已习惯离开gdb这样的调试工具,而靠静态代码分析和print来找root cause的习惯也让我对调试器不那么依赖。
之前的代码是直接存取ui数据的,没有区分model和view,我的第一个想法就是用mvc来把数据从视图中解耦出来,这里的模型数据,就是芯片参数值。通过从需求分析中提炼出的用户需要调试的参数,都在程序的内存中保留有一个对象。为什么要在内存中保留对象,而不是定义一个名叫chip的类,让所有参数都成为该chip类的porperty,它们的getter/setter定义为直接读写这些参数所对应的物理寄存器的方法呢?因为不是所有的ui元素都是能直接更新芯片的,有些需要用户confirm才能更新到chip中去。在设计模型的时候,我按照语义来划分参数的,比如有些参数要占用2个连续的寄存器来表示一个64位的值,有些参数只是一个寄存器的一个bit。
模型类设计成有一个字典接口,需要的参数则可以通过名字-值对访问。模型需要管理哪些参数是模型对象初始化的时候静态决定的,没有提供让客户可以动态加入参数的方法。
class Model:
def __init__ (self):
self.add_param('bob')
self.add_param('dumy')
def __getitem__(param_name, param_value):
''
def __getitem__(param_name, param_value):
''
model = Model()
model['bob'] = 1 # ==> set 1 into parameter 'bob'
val = model['dumy'] # ==> get value of parameter 'dumy'
模型对象初始化后,参数没有初始值,此时所有参数都是‘无效’的,用None来表示其值。模型对象的参数值来自于哪里它并不关心,它仅仅只负责提供设置参数值和取参数值的方法。对于客户程序来说,可以从chip中刷新,用户通过ui所做的设置,外部文件的加载。光把数据视图解耦还不够,还得有一个机制,每次数据发生变化时,用来反映该数据的不同ui元素都能自己更新自己(这才是mvc的威力,否则用它干嘛?),这样,不管某个参数的值是被哪里更新的,其对应的ui元素都立即会反映出它的值,这就是publisher-subscriber模式。幸运的是,python/wxPython有pubsub模块来很好的实现这种模式,不需要我重新轮一个。
有了pubsub实现的“订阅-通知”模型还不够,因为我发现我的需求还要一个更高抽象的概念,我把它称作“组通知“概念,其来源于绘制波形的需求,如下图:
其中有三个io信号s1,s2和s3,每个信号的占空比都来源于不止一个参数计算的结果,比如start, stop, refClock三个参数,这些参数更新时,subscriber都调用wave_draw函数来绘制它,在all refresh操作中,所有参数都会被从芯片中load一遍,那么draw函数只应该在s1,s2,s3均依赖的start, stop, refClock都被refresh结束后仅绘制一遍,而非在每个参数的refrehs后都绘制一遍,这样做除了为效率考虑外,还有一个原因就是当内存中的参数模型对象还未被成功构建结束之时,单个start, stop参数的更新不应该导致视图被绘制,因为此时模型的”更新会话“还没有结束,其他参数如refClock还是invalidate的,此时更新出来的view自然也是Invalidate的,而我不希望这种情况出现(虽然对用户来说,它应该看不到这些invalidate view的过程变化,但从语义上来讲,这是不合适的,并且,在绘制函数中,就要多写很多判断对象非None这样的代码)。那为什么不让每个信号的start, stop, refClock参数合起来作为一个大的参数呢?这样的话参数值的类型就无法用一个简单的int来表达了,我不希望对模型的参数值引入新的类型概念。
还有就是按照需求,有些参数必须满足一定的和其他参数之间的约束关系。这种约束关系可以用公式来描述,比如a * b = c / 2 + d等。这里参数a,b,c,d和常数2组成有一个约束链:
这个约束链里,除了常数2以外,a, b, c, d中的任何三个参数有了更新值并且都非None时,则可以算出第四个参数。那么这三个参数就组成一个组通知:通知消息在约束链中传递,通知更新函数则计算约束值。
在开始的一个版本中,因为看待问题不够深入,我引入了group概念,每个参数都属于某一个group,group邦定到一个名字,在构建数据模型时将参数和group联系起来,订阅者可以选择订阅单个参数的更新,也可以选择订阅一个group的更新。在通知的代码中,一个参数的更新除了会发送一个它自己的通知消息外,也将发送它对应的group的通知消息,那么为了让group的通知消息只被发送一次,我又引入了session的概念,比如:
model['param_1'] = 1
model['param_2'] = 2
model.update('param_1', 'param_2')
上面代码的model的set原语仅仅改变数据模型的值,调用update原语才能让通知消息被发送,数个set操作和一个update操作构成一个session。这样,如果param_1和param_2属于group_a,则group_a的通知消息只会被发送一遍。
这样做问题很多,首先,每个参数被限定死了只能绑定到一个group,而实际情况可能更复杂;其次把本来该是原子的”改写值-发送通知消息“的动作分为两步,相当于将内部实现暴露给了客户,使pubsub的抽象失去意义了;最后,为什么要让模型的更新者来知道session这个概念,更新者没有义务像上面那样写代码,它完全可以写成:
model['param_1'] = 1
model.update('param_1')
model['param_2'] = 2
model.update('param_2')
但这样就让group通知消息被发送了两次,使group的意义失效了。鉴于这么多的问题,后来重设计了这一部分代码,首先,仍然保留group的概念,但不为group绑定任何名字,只是在model.register_listener时,如果后面跟的参数不止一个,这些参数合在一块儿,构成了隐式的group,那么只有当所有的参数(group)都”有效“(非None)时,这个订阅者注册的更新函数才被调用:
class Model:
def register_listener(self, my_listener, *args):
'the my_listener callable is called when all *args are not None and notified'
def forget(self):
'forget the all parameter values of model, means invalidate them'
# client code:
# subscriber:
model.register_listener(update_1, 'param_1')
model.regsiter_listener(update_2, 'param_2')
model.regsiter_listener(update_all, 'param_1', 'param_2')
# updator -- trigger by such as refresh from chip or upload from user-supply file
model.forget()
model['param_1'] = 1 # this will cause update_1 be called but without update_all be called for param_2 is invalide
model['param_2'] = 2 # this will cause update_2 be called and update_all be called too
这样的接口让订阅者的语义更加自治,订阅者注册的函数仅当它所关心的所有参数都被更新了才得以调用。
这里增加了一个forget原语,比如在上面的参数约束链的例子中,客户程序要表达约束链里的所有参数都为“无效”状态,因为所有值在接下来都会被更新,任何中间状态导致的约束传递都是无意义的,于是,组通知更新函数就只会被调用一次了。虽然调用forget的责任也在模型的更新者,但一些用户操作的语义本身就很明显的暗示了这一行为,比如从芯片refresh所有的参数,从外部文件加载参数值等,所以也是合理的;而相较于前一种原语的设计,这种方式让客户程序产生错误的机会也更小,所以更好。
暂时先写到这里吧