1. 项目概述从“能算”到“好用”的跨越上次我们聊了如何在行空板上搭建一个基础的计算器实现了加减乘除这些核心功能。那个版本我们姑且称之为“计算器一”它解决的是“从无到有”的问题让行空板这块小巧的板子具备了基本的计算能力。但作为一个日常工具光“能算”是远远不够的。你有没有遇到过这样的场景想算个复杂的公式得来回切换输入一不小心就按错了或者想看看计算历史却找不到记录又或者界面太简陋用起来总觉得差点意思。这就是“计算器二”要解决的问题。我们的目标不再是实现一个功能而是打磨一个产品。它要更符合直觉的操作逻辑要有更强大的功能扩展性要有更友好的交互体验。简单说我们要让这个跑在行空板上的计算器从一个“演示程序”进化成一个真正“好用”的工具。这背后涉及到的远不止是几行代码的堆砌而是对用户交互、程序架构和硬件特性更深层次的思考与实践。行空板本身集成了屏幕、按键、丰富的传感器和GPIO这为我们打造一个不局限于屏幕触控的“立体交互”计算器提供了绝佳的硬件基础。我们完全可以跳出手机计算器的思维定式去探索一些更有趣的交互方式。同时结合网络热词中提到的各种专业计算器需求如CRC校验、工龄计算、单位换算等也启发我们可以为这个计算器设计一个可扩展的“插件”或“模式”系统让它未来能轻松变身成为各种专用计算工具。2. 核心设计思路交互、架构与扩展性2.1 交互设计的升级从单一触控到多维输入在第一个版本中我们很可能只使用了行空板的触摸屏进行输入。这次我们要充分利用其硬件特性。首先物理按键的引入至关重要。行空板自带三个物理按键A、B、C我们可以将它们定义为高频功能键。例如按键A作为“清除C”按键B作为“删除DEL”按键C作为“等于”或“模式切换”。在输入一长串数字时物理按键的确认感是触摸屏无法比拟的能有效减少误操作。特别是在进行连续计算时右手拇指可以自然地放在“等于”键上流畅度会大幅提升。其次利用传感器实现“体感交互”。这是一个非常有趣的探索点。例如我们可以利用板载的加速度传感器定义“摇一摇清除所有输入”的功能。当用户快速摇晃行空板时触发清屏操作这比在屏幕上寻找小小的“AC”按钮要快捷和有趣得多。再比如通过光线传感器感知环境光自动切换计算器界面的深色/浅色主题提升不同光照环境下的可视性。最后优化触摸屏的UI/UX。按钮要足够大间距要合理防止误触。对于高级函数如sin、cos、log等可以采用滑动标签页或长按弹出二级菜单的方式保持主界面简洁。状态反馈要明确比如按下按钮时有颜色或形状的变化显示区域有输入回显。2.2 程序架构的重构模块化与状态管理第一个版本的计算器代码可能将所有逻辑——界面绘制、事件处理、计算核心——都写在一个主循环里。当功能变得复杂时这种代码会难以阅读和维护。我们需要进行模块化拆分UI模块专门负责所有界面元素的绘制、更新和布局。这个模块接收“显示内容”和“界面状态”然后将其渲染到屏幕上。逻辑模块这是计算器的大脑。它包含一个表达式解析器负责处理用户输入的字符串如“35*sin(30)”将其分解成数字、运算符和函数并按照运算优先级进行计算。它还需要维护一个计算历史栈用于实现“回退”、“重做”或“历史查看”功能。输入处理模块这是一个事件调度中心。它需要同时监听触摸屏事件、物理按键事件和传感器事件。当检测到任何输入时它并不直接进行计算或更新界面而是将输入转化为一个标准的“操作指令”如{‘type’: ‘number’, ‘value’: ‘5’} 或 {‘type’: ‘operator’, ‘value’: ‘’}然后发送给逻辑模块处理。主控模块负责初始化以上所有模块并运行主循环。主循环中依次执行检查输入事件 - 发送指令给逻辑模块 - 逻辑模块处理并更新计算结果和状态 - 将新的显示数据和状态发送给UI模块 - UI模块刷新屏幕。这种架构的优势在于每个模块职责清晰。未来如果你想增加一个“科学计算模式”你主要修改的是UI模块绘制新按钮和逻辑模块增加新的函数解析输入模块和主控模块几乎不用动。这大大提升了代码的可扩展性。2.3 功能扩展性的预留迈向“计算器平台”观察网络热词可以发现人们对计算器的需求是高度分化的有CRC16校验、工龄计算、金属重量计算等非常具体的场景。我们不可能在一个版本内实现所有功能但可以设计一个能容纳这些功能的框架。一个可行的思路是引入“计算模式”的概念。在计算器界面上有一个“Mode”按钮点击后可以切换“标准模式”、“科学模式”、“程序员模式”以及未来的“校验计算模式”、“单位换算模式”等。每种模式对应一套不同的UI界面和逻辑处理规则。更进阶的设计是插件化。我们可以定义一套简单的插件接口允许将不同的计算功能如一个CRC16计算函数打包成一个独立的插件文件。主程序启动时加载这些插件并将其作为新的“模式”集成到菜单中。这样这个行空板计算器就从一个固定功能的App变成了一个可自由扩展的“计算平台”。3. 核心功能实现与代码解析3.1 表达式解析器计算器的灵魂这是整个项目最核心、也最具挑战性的部分。我们不能简单地按顺序计算必须处理运算符优先级乘除优先于加减和括号。一个经典且实用的算法是调度场算法。它的核心思想是将我们熟悉的中缀表达式如“3 5 * 2”转换为计算机更容易计算的后缀表达式逆波兰表达式如“3 5 2 * ”。我们来实现一个简化版的解析器class ExpressionParser: def __init__(self): # 定义运算符及其优先级 self.precedence {:1, -:1, *:2, /:2, ^:3} self.operators set(self.precedence.keys()) def infix_to_postfix(self, expression): 将中缀表达式转换为后缀表达式 output [] stack [] i 0 while i len(expression): ch expression[i] if ch.isdigit() or ch .: # 处理数字包括小数 num ch while i1 len(expression) and (expression[i1].isdigit() or expression[i1] .): i 1 num expression[i] output.append(num) elif ch (: stack.append(ch) elif ch ): while stack and stack[-1] ! (: output.append(stack.pop()) stack.pop() # 弹出左括号 elif ch in self.operators: # 处理运算符优先级 while (stack and stack[-1] ! ( and self.precedence.get(stack[-1], 0) self.precedence[ch]): output.append(stack.pop()) stack.append(ch) i 1 # 将栈中剩余运算符弹出 while stack: output.append(stack.pop()) return output def evaluate_postfix(self, postfix): 计算后缀表达式 stack [] for token in postfix: if token.replace(., ).isdigit(): # 是数字 stack.append(float(token)) else: # 是运算符 b stack.pop() a stack.pop() if token : result a b elif token -: result a - b elif token *: result a * b elif token /: if b 0: raise ValueError(除数不能为零) result a / b elif token ^: result a ** b stack.append(result) return stack[0] if stack else 0 def calculate(self, expression): 主计算函数 try: postfix self.infix_to_postfix(expression) return self.evaluate_postfix(postfix) except Exception as e: return f错误: {e}这个ExpressionParser类就是我们的计算引擎。calculate方法是对外接口输入一个字符串表达式返回计算结果。它内部先调用infix_to_postfix进行转换再调用evaluate_postfix进行计算。这样我们就能正确处理35*2结果是13而不是16这样的表达式了。注意这是一个基础版本尚未处理负数开头如“-53”和函数调用如“sin(30)”。处理这些需要更复杂的词法分析但核心框架已搭建好。对于函数我们可以在转换后缀表达式前先将函数名如“sin”视为一个特殊运算符或者单独进行预处理。3.2 多输入事件融合处理行空板的pinpong库提供了统一的事件监听机制。我们需要在主循环中同时处理多种输入。from pinpong.board import Board, Pin from pinpong.extension.unihiker import * import time Board().begin() # 初始化行空板 # 初始化UI这里用unihiker的GUI库示例 from unihiker import GUI gui GUI() # 定义物理按键引脚 btn_clear Pin(Pin.P21, Pin.IN) # 假设按键A接在P21作为清除键 btn_equals Pin(Pin.P22, Pin.IN) # 按键B作为等于键 # 加速度传感器 from pinpong.libs.dfrobot_ads1115 import DFRobot_ADS1115 import math accel DFRobot_ADS1115() # 示例实际需根据行空板传感器库调整 # 状态变量 current_input last_shake_time 0 shake_threshold 15 # 摇晃灵敏度 def on_screen_click(x, y): 处理屏幕触摸事件 global current_input # 这里需要根据(x,y)坐标判断点击了哪个按钮 # button get_button_by_position(x, y) # 假设的函数 # if button 7: # current_input 7 # ... 其他按钮逻辑 # 更新UI显示 update_display(current_input) def check_shake(): 检查摇晃动作 global last_shake_time, current_input # 读取加速度值 (示例实际API可能不同) # x, y, z accel.read_accel() # total_accel math.sqrt(x**2 y**2 z**2) # if total_accel shake_threshold and time.time() - last_shake_time 1: # current_input # 清空输入 # last_shake_time time.time() # update_display(0) # gui.show_notification(已清空, duration1) pass def main_loop(): parser ExpressionParser() while True: # 1. 检查物理按键 if btn_clear.value() 0: # 按键按下假设低电平有效 time.sleep(0.05) # 简单消抖 if btn_clear.value() 0: current_input update_display(0) while btn_clear.value() 0: # 等待按键释放 time.sleep(0.01) if btn_equals.value() 0: time.sleep(0.05) if btn_equals.value() 0: result parser.calculate(current_input) current_input str(result) update_display(current_input) while btn_equals.value() 0: time.sleep(0.01) # 2. 检查体感操作摇晃清空 check_shake() # 3. 屏幕触摸事件由unihiker GUI库的回调函数处理已在上方定义 # 4. 更新UI等其他任务 time.sleep(0.02) # 主循环延迟降低CPU占用这个主循环框架展示了如何将触摸屏、物理按键和传感器事件统一处理。关键在于将不同来源的输入都转化为对current_input这个全局状态变量的操作然后统一调用update_display来刷新界面。3.3 历史记录与回退功能实现一个实用的计算器应该能追溯计算步骤。我们可以用一个列表来保存历史记录。class CalculatorHistory: def __init__(self, max_length50): self.history [] # 列表项为字典如 {expr: 35*2, result: 13} self.max_length max_length self.current_index -1 # 当前查看的历史索引 def add_record(self, expression, result): 添加一条计算记录 record {expr: expression, result: str(result)} # 如果历史记录太长移除最老的 if len(self.history) self.max_length: self.history.pop(0) self.history.append(record) self.current_index len(self.history) - 1 def get_previous(self): 获取上一条记录 if self.history and self.current_index 0: self.current_index - 1 return self.history[self.current_index] elif self.history: # 已经在第一条 return self.history[0] return None def get_next(self): 获取下一条记录 if self.history and self.current_index len(self.history) - 1: self.current_index 1 return self.history[self.current_index] return None def clear(self): 清空历史 self.history.clear() self.current_index -1在计算完成后调用history.add_record(current_input, result)进行保存。然后我们可以通过两个新的物理按键或者屏幕上的按钮来绑定get_previous和get_next方法实现历史记录的浏览。当用户选择一条历史记录时可以将expr直接加载到当前输入区域方便重新计算或修改。4. 界面美化与用户体验优化4.1 使用Unihiker GUI库构建美观界面行空板官方推荐的unihiker库提供了比底层Tkinter更便捷的GUI组件。from unihiker import GUI import time gui GUI() display_text gui.draw_text(x120, y50, text0, font_size30, color#000000) # 定义按钮样式和布局 buttons [ {text:7, x:30, y:120, w:60, h:60, cmd:7}, {text:8, x:100, y:120, w:60, h:60, cmd:8}, {text:9, x:170, y:120, w:60, h:60, cmd:9}, {text:/, x:240, y:120, w:60, h:60, cmd:/}, {text:4, x:30, y:190, w:60, h:60, cmd:4}, {text:5, x:100, y:190, w:60, h:60, cmd:5}, {text:6, x:170, y:190, w:60, h:60, cmd:6}, {text:*, x:240, y:190, w:60, h:60, cmd:*}, {text:1, x:30, y:260, w:60, h:60, cmd:1}, {text:2, x:100, y:260, w:60, h:60, cmd:2}, {text:3, x:170, y:260, w:60, h:60, cmd:3}, {text:-, x:240, y:260, w:60, h:60, cmd:-}, {text:0, x:30, y:330, w:130, h:60, cmd:0}, {text:., x:170, y:330, w:60, h:60, cmd:.}, {text:, x:240, y:330, w:60, h:60, cmd:}, {text:, x:240, y:400, w:60, h:60, cmd:, bg:#FF9800}, # 等于键高亮 {text:C, x:30, y:400, w:60, h:60, cmd:C}, {text:DEL, x:100, y:400, w:60, h:60, cmd:DEL}, {text:Mode, x:170, y:400, w:60, h:60, cmd:MODE}, ] btn_objects [] for btn in buttons: obj gui.draw_button(xbtn[x], ybtn[y], wbtn[w], hbtn[h], textbtn[text], origintop_left, bgbtn.get(bg, #E0E0E0)) obj.config(cmdlambda cmdbtn[cmd]: on_button_click(cmd)) btn_objects.append(obj) def update_display(content): 更新显示屏内容 # 限制显示长度过长时缩小字体或滚动显示 if len(content) 20: display_text.config(textcontent[-20:], font_size20) else: display_text.config(textcontent, font_size30) def on_button_click(cmd): global current_input if cmd.isdigit() or cmd .: current_input cmd elif cmd in -*/: current_input cmd # 运算符前后加空格便于阅读和解析 elif cmd : # 调用表达式解析器计算 result parser.calculate(current_input.replace( , )) history.add_record(current_input, result) current_input str(result) elif cmd C: current_input elif cmd DEL: current_input current_input[:-1] elif cmd MODE: switch_mode() # 切换模式函数 update_display(current_input if current_input else 0)这个界面布局清晰按钮大小适合触摸操作。我们为“等于”键设置了不同的背景色橙色使其在视觉上更突出。update_display函数还加入了简单的自适应逻辑当输入过长时自动缩小字体防止显示不全。4.2 动画与反馈增强体验静态的界面是枯燥的。我们可以加入微交互来提升体验。def animate_button_press(btn_obj): 按钮按下动画 original_color btn_obj.cget(bg) btn_obj.config(bg#BDBDBD) # 按下时变深色 gui.update() time.sleep(0.05) btn_obj.config(bgoriginal_color) # 恢复原色 gui.update() def on_button_click(cmd): global current_input # ... 原有的逻辑处理 ... # 在处理完逻辑后找到对应的按钮对象并触发动画这里需要建立cmd到对象的映射 # animate_button_press(btn_map[cmd])此外在计算完成或发生错误时可以给出更明确的提示。例如计算成功时让结果显示区域有一个短暂的背景色闪烁如淡绿色计算错误时显示区域闪烁淡红色并配合振动反馈如果行空板支持马达。5. 高级功能探索与扩展5.1 实现科学计算模式在“科学模式”下我们需要增加新的函数按钮和常量。这需要扩展我们的表达式解析器。首先在逻辑模块中扩充支持的函数和常量字典import math class ScientificParser(ExpressionParser): def __init__(self): super().__init__() # 扩展函数和常量 self.functions { sin: math.sin, cos: math.cos, tan: math.tan, log: math.log10, ln: math.log, sqrt: math.sqrt, exp: math.exp, } self.constants { pi: math.pi, e: math.e } # 将函数名也视为一种高优先级“运算符” for func in self.functions: self.precedence[func] 4 def infix_to_postfix(self, expression): 重写增加对函数和常量的识别 # 预处理将常量替换为数值 for const, value in self.constants.items(): expression expression.replace(const, str(value)) # 识别函数名更复杂的词法分析这里简化处理 # 假设函数格式为 func(expr)如 sin(30) # 实际实现需要更精细的tokenizer此处略过详细代码 # ... return super().infix_to_postfix(processed_expression) def evaluate_postfix(self, postfix): 重写增加函数计算 stack [] for token in postfix: if token.replace(., ).isdigit(): stack.append(float(token)) elif token in self.functions: # 函数操作取一个操作数 a stack.pop() # 注意三角函数可能需要角度转弧度 if token in [sin, cos, tan]: a math.radians(a) # 假设输入是角度 result self.functions[token](a) stack.append(result) else: # 原有的二元运算符逻辑 b stack.pop() a stack.pop() # ... 运算符计算 ... stack.append(result) return stack[0]在UI上科学模式可以切换另一套按钮布局或者通过滑动/下拉菜单显示第二功能面板。5.2 程序员模式与单位换算程序员模式主要涉及进制转换二、八、十、十六进制和位运算。核心是维护一个当前数值以及当前的进制状态。当用户输入数字或进行位运算时都在内部以整数形式处理只是在显示时根据当前进制进行转换。class ProgrammerMode: def __init__(self): self.current_value 0 self.base 10 # 当前显示进制2, 8, 10, 16 def input_digit(self, digit, base_from): 输入数字根据输入进制更新内部值 # 例如在十六进制下用户按了A digitA, base_from16 self.current_value self.current_value * base_from int(digit, base_from) def change_base(self, new_base): 切换显示进制 self.base new_base def get_display(self): 获取当前进制下的显示字符串 if self.base 2: return bin(self.current_value)[2:] elif self.base 8: return oct(self.current_value)[2:] elif self.base 16: return hex(self.current_value)[2:].upper() else: # 十进制 return str(self.current_value)单位换算模式如温度、长度、重量则可以预先定义好换算关系字典。例如温度转换unit_conversions { temperature: { C_to_F: lambda c: c * 9/5 32, F_to_C: lambda f: (f - 32) * 5/9, C_to_K: lambda c: c 273.15, K_to_C: lambda k: k - 273.15, }, length: { m_to_km: lambda m: m / 1000, km_to_m: lambda km: km * 1000, # ... 其他 } }界面设计上可以提供两个下拉菜单选择源单位和目标单位一个输入框一个输出框和一个“转换”按钮。5.3 针对网络热词的专用功能构思网络热词反映了真实需求我们可以从中汲取灵感设计一些特色功能CRC16校验码计算器在“工具”或“程序员”模式下增加一个功能。用户输入一串十六进制数据或ASCII字符串选择CRC16的变种如Modbus、CCITT点击计算即可得到校验码。核心是实现CRC16算法。工龄/工作年限计算器这是一个典型的日期计算需求。提供两个日期选择器开始日期、结束日期/当前日期计算相差的年、月、日。需要注意闰年和月份天数的处理。金属重量计算器属于单位换算和几何计算的结合。用户选择材料如铁、铜、铝内置密度表、形状板材、棒材、管材输入尺寸长宽高、直径、厚度等自动计算重量。公式为体积 * 密度。这些功能可以作为独立的“计算模式”集成进来每个模式有自己专属的输入界面和计算逻辑。这正体现了我们之前设计的“可扩展架构”的优势。6. 性能优化与调试技巧6.1 内存与性能管理行空板的资源CPU、内存相对有限在开发时需要注意避免全局重绘UI更新时只更新变化的部分。例如只有显示区域的内容变化了才去更新display_text对象而不是刷新整个屏幕。使用生成器或迭代器处理长列表例如在浏览超长的计算历史时不要一次性将所有历史记录渲染到屏幕上只渲染当前可视区域附近的几条。图片资源优化如果使用了背景图或图标务必使用适合屏幕大小240*320的图片并考虑转换为低色深的格式如索引色PNG以节省内存和加载时间。主循环延迟主循环中的time.sleep(0.02)约50Hz是一个平衡点。太短会无谓消耗CPU太长会导致界面响应迟钝。可以根据实际响应速度调整。6.2 常见问题与调试触摸不灵敏或坐标错乱问题点击按钮没反应或者点A按钮触发B按钮。排查首先打印出触摸事件的坐标(x, y)确认unihiker库返回的坐标原点通常是左上角和坐标系是否与你的按钮布局匹配。检查按钮的碰撞检测区域是否计算正确。解决确保按钮的(x, y)定义和触摸事件坐标在同一坐标系下。可以给每个按钮绘制一个半透明的调试框直观查看响应区域。表达式解析错误问题输入35或sin(30缺少括号导致程序崩溃或返回错误。排查在ExpressionParser.calculate()方法中用try...except捕获所有异常并返回友好的错误信息如“表达式格式错误”。在开发阶段可以将解析的中间步骤如分词后的列表、后缀表达式打印出来便于定位问题。解决增强表达式预处理。例如合并连续的操作符检查括号是否匹配。对于函数调用确保括号成对出现。物理按键抖动问题按一次物理按键触发了多次事件。解决加入软件消抖。如上文代码所示在检测到按键按下后延时一小段时间如50ms再次检测按键状态如果仍然为按下状态才确认为有效按键。这是嵌入式开发中处理机械开关的常规操作。程序卡顿或无响应问题进行复杂计算如计算1000的阶乘时界面“冻住”了。解决将耗时的计算任务放到一个单独的线程中执行避免阻塞主UI线程。在行空板的MicroPython环境中可以使用_thread模块。计算开始时显示一个“计算中...”的提示计算完成后在主线-程中更新结果。6.3 部署与打包建议当项目开发完成后你可能希望它像一个真正的App一样开机自启或者方便地分享给他人。开机自启将主程序文件命名为main.py并放在行空板的根目录。行空板系统启动后会自动运行main.py。依赖管理如果你的计算器使用了第三方库除了unihiker/pinpong需要将这些库文件也一并上传到行空板。资源文件图片、字体等资源文件建议放在一个单独的目录如/res/中方便管理。版本与配置可以考虑增加一个简单的“关于”页面显示计算器版本。也可以将用户设置如默认模式、主题颜色保存到一个JSON配置文件中实现设置的持久化。从“计算器一”到“计算器二”我们完成了一次从功能实现到产品打磨的旅程。这个过程不仅仅是代码量的增加更是对问题理解、架构设计和用户体验的全面深化。行空板作为一个软硬件一体的平台给了我们巨大的创新空间让我们能跳出纯软件应用的框框去思考如何让交互更自然、更高效。当你亲手实现一个功能并看到它被流畅使用时那种成就感是无可替代的。这个项目还有很多可以继续深挖的方向比如语音输入计算表达式、连接网络获取实时汇率进行换算、甚至利用行空板的Wi-Fi功能做成一个简单的网络计算服务端。希望这个详细的实现思路和代码解析能为你基于行空板创作更多有趣、有用的“小应用”打开一扇门。