我的openpnp设备接入的飞达是西门子二手飞达, 用openpnp提供的SlotSchultzFeeder.
发现原版openpnp有个问题(bug):
接入多个西门子飞达时, 因为要调整飞达参数(或仅仅就想确认一下参数), 切换到不同飞达时, 大概率会弹框报错.
报错的项目有多种(最多4种: 飞达ID取不到, 飞达送料数取不到, 步长取不到, 飞达状态取不到).
如果不使用openpnp, 而使用串口助手, 怎么发指令给mage2560控制底板, 飞达都可以正常控制, 回包都正常.
其实这个问题早发现了, 因为当时没到要大规模使用西门子二手飞达这步, 只要能通讯, 就说明飞达正常. 也没想去解决.
现在要将西门子二手飞达都挂上了, 要是时不时弹框报错, 那手工干预不起啊(这种骚扰顶不住, 这不成了被openpnp和飞达设备耍了么?)
现在必须要解决这个问题.
开始怀疑是mega2560控制板通讯处理有问题, 查了一下. 确实有问题.
但是不是编程逻辑的问题, 而是arduino库的问题.
mega2560的arduino库, 无法做到能快速正确的处理多个连续(100ms之内)的串口命令.
这可能也不是arduino库的问题, 随便哪个MCU, 串口发送的包间隔速度小于MCU的处理单条串口命令的总时间, 都不能保证每一个命令都处理正确.
观察了一下openpnp的日志, 发现openpnp会在同一时间(10ms~20ms之内), 连续发送2条串口命令给飞达控制板. 这哪个MCU也扛不住啊.
用串口助手试了一下, 循环发送命令, 只要发送间隔>200ms, 没有一次会让飞达控制板出错. 这就对了, 不能狂发命令给MCU啊.
这就需要改openpnp源码了, 将在同一时间连续不停的发送2条命令的地方找到, 简单处理一下, 在每条命令发送之前, 都至少需要间隔250ms以上才行. 间隔就用java自带的sleep处理一下.
对java不熟, 不想没事找事. 这次被openpnp逼的实在没招了. 非得自己动手才有吃的.
还好是维护性质的, 查引起bug的原因, 改点逻辑, 这个咱能搞.
这个环境早就做了实验+笔记(openpnp - 软件调试环境搭建), 在本地的环境还没动, 可以直接照着笔记实验, 开心. 真机智, 早就预料到了会有改openpnp源码那一天.
根据日志和报错提示, 在工程中用字符串搜索大法 + 断点法. 让报错时, 停在断点上, 那就说明找对了地方.
用的IDE是, 挺好用的.
我现在用的openpnp发布版本是 openpnp-dev-2022-0801
openpnp dev 代码的git url 为 : https://github.com/openpnp/openpnp.git, 先迁出到本地.
最新的代码日期为2023/3/15
openpnp-dev-2022-0801 对应的安装包为 OpenPnP-windows-x64-develop_2022-08-01_18-07-09.2a36a8d.exe
在git记录中查到2022-8-1上午, 是2022-8-1最后的实现代码. 在2022-8-1最后提交的代码处做了本地分支, 命名为openpnp_dev_2022_0801
在这个分支上做自己的修改.
openpnp的sendcommand函数, 参数只有回包超时时间, 而没有发送前需要sleep的时间. 这就导致发送者只要发送命令, 立刻就会被执行. 如果连续执行多条命令, 就会引起下位机处理不过来, 上位机也会处理错(因为接收是异步处理, 回包格式也不能区分出是哪个包, 就会错将不是自己的命令回包, 当作自己的, 这样从回包中取到的值的目标就错了, e.g. 飞达参数填错了位置).
确有连续狂发命令的地方.
在切换飞达条目时, 飞达界面上的4个数据(飞达ID, 飞达送料数, 步长, 飞达状态), 都不是存起来显示的, 而是切到新飞达条目, 就立刻取数据.
这就导致在飞达列表中切换到不同飞达条码时, 大概率会是mega2560处理不过来, 导致没回包, 或回包出错(e.g. 连发送的命令都没识别完整, 估计是被覆盖了, 因为缓冲区就64个字节, 有4个命令输入的缓冲区, 其实就一个缓冲区就够了, 主要是上位机发送的命令间隔太短了, 超出了下位机的通讯处理速度(下位机mage2560要将上位机命令转换为西门子飞达的实际通讯指令, 等飞达回包, 再转换为上位机能理解的回包, 这些都需要时间啊), 总是会出错, 这是跑不了的, 只是概率大小问题.
使用不同家做的冰沙主板时, 明明主板是好的, 串口也没问题, 但是有时要2~3次或者更多次才能和主板连接上. 最坏的情况是一直连不上主板(遇到过, 什么时候能连接上主板通讯, 都是靠运气)
准备给冰沙主板发连接命令时, 也要sleep之后, 才发给主板.
就按照git提交记录来, 不分先后.
driver.sendCommand(cmd, 5000, 300); // 参数3原来是没有的, 现在为发送前的sleep时间
sendGcode(command, timeout, 0); // 如果确认这里只发一条, 且时机前后不会再发送其他串口指令, 发送前sleep时间就可以设置为0, 这个需要改完来验证.
// 相关发送命令的函数都加上发送前sleep的参数
protected void sendGcode(String gCode, long timeout, long time_sleep_before_send) throws Exception {
if (gCode == null) {
return;
}
for (String command : gCode.split("\n")) {
command = command.trim();
if (command.length() == 0) {
continue;
}
sendCommand(command, timeout, time_sleep_before_send);
}
}
public void sendCommand(String command) throws Exception {
sendCommand(command, timeoutMilliseconds, 0);
}
public void sendCommand(String command, long timeout, long time_sleep_before_send) throws Exception {
// An error may have popped up in the meantime. Check and bail on it, before sending the next command.
bailOnError();
if (command == null) {
return;
}
Logger.debug("[{}] >> {}, {}, {}", getCommunications().getConnectionName(), command, timeout, time_sleep_before_send);
// 发送前sleep的实现, 就用Thread.sleep简单处理一下.
if (time_sleep_before_send > 0)
{
Thread.sleep(time_sleep_before_send);
}
command = preProcessCommand(command);
@Override
public void sendCommand(String command, long timeout, long time_sleep_before_send) throws Exception {
if (waitedForCommands) {
// We had a wait for commands and caller had the last chance to receive responses.
waitedForCommands = false;
// If the caller did not get them, clear them now.
responseQueue.clear();
}
bailOnError();
if (command == null) {
return;
}
Logger.debug("{} commandQueue.offer({}, {})...", getCommunications().getConnectionName(), command, timeout);
if (time_sleep_before_send > 0)
{
Thread.sleep(time_sleep_before_send);
}
这个实现中, 有连发4条命令的地方, 封装一个私有函数, 用来ms延时
private void my_delay_ms(long ms)
{
try {
Thread.sleep(ms);
}
catch(InterruptedException e)
{
// nothing, only catch
// java: unreported exception java.lang.InterruptedException; must be caught or declared to be thrown
}
}
public SlotSchultzFeederConfigurationWizard(SlotSchultzFeeder feeder) {
// 这里是在飞达列表中切换会来的函数, 主要是填界面参数, 如果是在飞达设备中的参数, 从飞达中取出来, 再填入界面.
// ...
statusText = new JTextField();
statusText.setColumns(50);
panelActuator.add(statusText, "8, 20");
if(Configuration.get().getMachine().isEnabled()){
// 命令不能并发, 下位机处理不过来.
// openpnp原始实现连发了4条指令给飞达, 导致飞达处理不过来
// 修正后, 在执行命令之前, 都sleep 300ms, 看日志, 249ms估计也可以.
my_delay_ms(300); // add by ls
getIdActuatorAction.actionPerformed(null);
my_delay_ms(300); // add by ls
getFeedCountActuatorAction.actionPerformed(null);
my_delay_ms(300); // add by ls
pitchActuatorAction.actionPerformed(null);
my_delay_ms(300); // add by ls
statusActuatorAction.actionPerformed(null);
}
for (Bank bank : SlotSchultzFeeder.getBanks()) {
bankCb.addItem(bank);
}
// ...
public synchronized void connect() throws Exception {
disconnectRequested = false;
getCommunications().connect();
connected = false;
connectThreads();
// Wait a bit while the controller starts up
Thread.sleep(connectWaitTimeMilliseconds);
// Consume any startup messages
try {
while (!receiveResponses().isEmpty()) {
}
}
catch (Exception e) {
}
// Disable the machine
setEnabled(false);
// Send startup Gcode
// 加了参数3(发送前sleep的时间), 可能是connectThreads()和这里的发送命令有冲突, e.g. connectThreads()还没有关掉串口之类的
// 要不就没法解释清楚为啥有时openpnp抛出串口被占用, 或者链接不上的情况.
sendGcode_Ex(getCommand(null, CommandType.CONNECT_COMMAND), 200);
connected = true;
}
@Override
public void setEnabled(boolean enabled) throws Exception {
if (enabled && !connected) {
connect();
}
if (connected) {
if (enabled) {
// Assume a freshly re-enabled machine has no pending moves anymore.
motionPending = false;
sendGcode_Ex(getCommand(null, CommandType.ENABLE_COMMAND), 200); // 在可能会在连续发送命令的时机, 加上发送前的sleep
}
else {
try {
sendGcode_Ex(getCommand(null, CommandType.DISABLE_COMMAND), 200);// 在可能会在连续发送命令的时机, 加上发送前的sleep
drainCommandQueue(getTimeoutAtMachineSpeed());
}
catch (Exception e) {
// When the connection is lost, we have IO errors. We should still be able to go on
// disabling the machine.
Logger.warn(e);
}
}
}
if (connected && !enabled) {
if (isInSimulationMode() || !connectionKeepAlive) {
disconnect();
}
}
super.setEnabled(enabled);
}
// actutor执行的地方, 都有可能是连续发送命令的组合, 都加上睡完发送参数值
@Override
public void actuate(Actuator actuator, boolean on) throws Exception {
String command = getCommand(actuator, CommandType.ACTUATE_BOOLEAN_COMMAND);
command = substituteVariable(command, "Id", actuator.getId());
command = substituteVariable(command, "Name", actuator.getName());
if (actuator instanceof ReferenceActuator) {
command = substituteVariable(command, "Index", ((ReferenceActuator)actuator).getIndex());
}
command = substituteVariable(command, "BooleanValue", on);
command = substituteVariable(command, "True", on ? on : null);
command = substituteVariable(command, "False", on ? null : on);
sendGcode_Ex(command, 200); // param2, sleep then send
SimulationModeMachine.simulateActuate(actuator, on, true);
}
@Override
public void actuate(Actuator actuator, double value) throws Exception {
String command = getCommand(actuator, CommandType.ACTUATE_DOUBLE_COMMAND);
command = substituteVariable(command, "Id", actuator.getId());
command = substituteVariable(command, "Name", actuator.getName());
if (actuator instanceof ReferenceActuator) {
command = substituteVariable(command, "Index", ((ReferenceActuator)actuator).getIndex());
}
command = substituteVariable(command, "DoubleValue", value);
command = substituteVariable(command, "IntegerValue", (int) value);
sendGcode_Ex(command, 200); // param2, sleep then send
SimulationModeMachine.simulateActuate(actuator, value, true);
}
@Override
public void actuate(Actuator actuator, String value) throws Exception {
String command = getCommand(actuator, CommandType.ACTUATE_STRING_COMMAND);
command = substituteVariable(command, "Id", actuator.getId());
command = substituteVariable(command, "Name", actuator.getName());
if (actuator instanceof ReferenceActuator) {
command = substituteVariable(command, "Index", ((ReferenceActuator)actuator).getIndex());
}
command = substituteVariable(command, "StringValue", value);
sendGcode_Ex(command, 200); // param2, sleep then send
}
@Override
public String actuatorRead(Actuator actuator, Object parameter) throws Exception {
/*
* The logic here is a little complicated. This is the only driver method that is
* not fire and forget. In this case, we need to know if the command was serviced or not
* and throw an Exception if not.
*/
String command = getCommand(actuator, CommandType.ACTUATOR_READ_COMMAND);
String regex = getCommand(actuator, CommandType.ACTUATOR_READ_REGEX);
if (command != null && regex != null) {
command = substituteVariable(command, "Id", actuator.getId());
command = substituteVariable(command, "Name", actuator.getName());
if (actuator instanceof ReferenceActuator) {
command = substituteVariable(command, "Index", ((ReferenceActuator)actuator).getIndex());
}
if (parameter != null) {
if (parameter instanceof Double) { // Backwards compatibility
Double doubleParameter = (Double) parameter;
command = substituteVariable(command, "DoubleValue", doubleParameter);
command = substituteVariable(command, "IntegerValue", (int) doubleParameter.doubleValue());
}
command = substituteVariable(command, "Value", parameter);
}
sendGcode_Ex(command, 200); // actor相关的命令, 都加上睡后发送
// 原始的实现为了方便调用, 只加了回包超时参数, 现在加入一个新参数time_sleep_before_send
protected void sendGcode_Ex(String gCode, long time_sleep_before_send) throws Exception {
sendGcode_Ex(gCode, timeoutMilliseconds, time_sleep_before_send);
}
protected void sendGcode_Ex(String gCode, long timeout, long time_sleep_before_send) throws Exception {
if (gCode == null) {
return;
}
for (String command : gCode.split("\n")) {
command = command.trim();
if (command.length() == 0) {
continue;
}
sendCommand(command, timeout, time_sleep_before_send);
}
}
public void sendCommand(String command) throws Exception {
sendCommand(command, timeoutMilliseconds, 0);
}
public void sendCommand(String command, long timeout, long time_sleep_before_send) throws Exception {
// An error may have popped up in the meantime. Check and bail on it, before sending the next command.
bailOnError();
if (command == null) {
return;
}
Logger.debug("[{}] >> {}, {}, {}", getCommunications().getConnectionName(), command, timeout, time_sleep_before_send);
if (command == "M610N3")
{
Logger.debug("bp");
}
if (time_sleep_before_send > 0)
{
Thread.sleep(time_sleep_before_send);
}
// ...
改过的程序, 给个新版本号, 和官方实现区分开.
public class Main {
public static String getVersion() {
String version = Main.class.getPackage().getImplementationVersion();
if (version == null) {
// 没看清getImplementationVersion()从哪取的版本信息, 先硬写一个临时版本号
version = "INTERNAL BUILD - base 2022-8-1 last, ls 2023_1026_0608PM";
}
return version;
}
用IEDA带着程序跑起来(调试状态, run状态), openpnp控制设备都好使.
此时, 在添加好的飞达列表中的飞达之间切换, 会卡大概1秒钟, 然后就会正常显示飞达信息.
这个1秒多时间的卡, 如果是自动贴片的时候(e.g. 自动取料时, 如果换了一个飞达供料, 也可能会重新取飞达参数, 遇到过在自动贴片过程中, 弹出取不到飞达ID的情况), 根本感觉不到.
问题已经解决了, 暂时未发现不良影响
剩下的是事情就是将修改完的程序发布给自己用, 脱离IDE的环境. 这个也做完实验了, 确定可以简易发布给自己其他计算机用.
这个在下一篇笔记中记录, 和修改源码没关系. 也是为了自己以后好按照关键字来找笔记, e.g. 在oepnpnp栏目搜索包含"打包"关键字的笔记, 就能找到如何发布openpnp程序给自己用.