|
@@ -325,6 +325,275 @@ app.registerDeviceConfigurationWidget("modbusnginput",
|
325
|
325
|
inserter = new NodeInserter(tr("ModbusNG Port"), tr("Modbus RTU Port"), "modbusngport", NULL);
|
326
|
326
|
topLevelNodeInserters.append(inserter);
|
327
|
327
|
|
|
328
|
+@ While the old design only needed to deal with a small number of potential
|
|
329
|
+messages and responses, it makes sense for the new design to use a scan list
|
|
330
|
+of arbitrary length. An initial implementation can simply store the data needed
|
|
331
|
+to make requests, properly interpret the results, and output data to the
|
|
332
|
+correct channels. There is room to improve operational efficiency later by
|
|
333
|
+batching operations on adjacent addresses on the same function into a single
|
|
334
|
+request, but there are known devices in use in coffee roasters which do not
|
|
335
|
+support reading from multiple registers simultaneously, so there must also be a
|
|
336
|
+way to turn such optimizations off.
|
|
337
|
+
|
|
338
|
+@<Class declarations@>=
|
|
339
|
+enum ModbusDataFormat
|
|
340
|
+{
|
|
341
|
+ Int16,
|
|
342
|
+ FloatHL,
|
|
343
|
+ FloatLH
|
|
344
|
+};
|
|
345
|
+
|
|
346
|
+struct ModbusScanItem
|
|
347
|
+{
|
|
348
|
+ QByteArray request;
|
|
349
|
+ ModbusDataFormat format;
|
|
350
|
+ int decimalPosition;
|
|
351
|
+ Units::Unit unit;
|
|
352
|
+ mutable double lastValue;
|
|
353
|
+};
|
|
354
|
+
|
328
|
355
|
@ Another class is used to handle the communication with the bus and serve as
|
329
|
356
|
an integration point for \pn{}.
|
330
|
357
|
|
|
358
|
+@<Class declarations@>=
|
|
359
|
+class ModbusNG : public QObject
|
|
360
|
+{
|
|
361
|
+ Q_OBJECT
|
|
362
|
+ public:
|
|
363
|
+ ModbusNG(DeviceTreeModel *model, const QModelIndex &index);
|
|
364
|
+ ~ModbusNG();
|
|
365
|
+ QList<Channel*> channels;
|
|
366
|
+ QList<ModbusScanItem> scanList;
|
|
367
|
+ QList<QString> channelNames;
|
|
368
|
+ QList<bool> hiddenStates;
|
|
369
|
+ private slots:
|
|
370
|
+ void sendNextMessage();
|
|
371
|
+ void timeout();
|
|
372
|
+ void dataAvailable();
|
|
373
|
+ private:
|
|
374
|
+ quint16 calculateCRC(QByteArray data);
|
|
375
|
+ QextSerialPort *port;
|
|
376
|
+ int delayTime;
|
|
377
|
+ QTimer *messageDelayTimer;
|
|
378
|
+ QTimer *commTimeout;
|
|
379
|
+ int scanPosition;
|
|
380
|
+ QByteArray responseBuffer;
|
|
381
|
+};
|
|
382
|
+
|
|
383
|
+@ One of the things that the old Modbus code got right was in allowing the
|
|
384
|
+constructor to handle device configuration by accepting its configuration
|
|
385
|
+sub-tree. In this design, child nodes establish a scan list.
|
|
386
|
+
|
|
387
|
+@<ModbusNG implementation@>=
|
|
388
|
+ModbusNG::ModbusNG(DeviceTreeModel *model, const QModelIndex &index) :
|
|
389
|
+ QObject(NULL), messageDelayTimer(new QTimer), commTimeout(new QTimer),
|
|
390
|
+ scanPosition(0)
|
|
391
|
+{
|
|
392
|
+ QDomElement portReferenceElement =
|
|
393
|
+ model->referenceElement(model->data(index, Qt::UserRole).toString());
|
|
394
|
+ QDomNodeList portConfigData = portReferenceElement.elementsByTagName("attribute");
|
|
395
|
+ QDomElement node;
|
|
396
|
+ QVariantMap attributes;
|
|
397
|
+ for(int i = 0; i < portConfigData.size(); i++)
|
|
398
|
+ {
|
|
399
|
+ node = portConfigData.at(i).toElement();
|
|
400
|
+ attributes.insert(node.attribute("name"), node.attribute("value"));
|
|
401
|
+ }
|
|
402
|
+ port = new QextSerialPort(attributes.value("port").toString(),
|
|
403
|
+ QextSerialPort::EventDriven);
|
|
404
|
+ port->setBaudRate((BaudRateType)(attributes.value("baud").toInt()));
|
|
405
|
+ port->setDataBits(DATA_8);
|
|
406
|
+ port->setParity((ParityType)attributes.value("parity").toInt());
|
|
407
|
+ port->setStopBits((StopBitsType)attributes.value("stop").toInt());
|
|
408
|
+ port->setFlowControl((FlowType)attributes.value("flow").toInt());
|
|
409
|
+ delayTime = (int)(((double)(1)/(double)(attributes.value("baud").toInt())) * 144000.0);
|
|
410
|
+ messageDelayTimer->setSingleShot(true);
|
|
411
|
+ commTimeout->setSingleShot(true);
|
|
412
|
+ connect(messageDelayTimer, SIGNAL(timeout()), this, SLOT(sendNextMessage()));
|
|
413
|
+ connect(commTimeout, SIGNAL(timeout()), this, SLOT(timeout()));
|
|
414
|
+ connect(port, SIGNAL(readyRead()), this, SLOT(dataAvailable()));
|
|
415
|
+ port->open(QIODevice::ReadWrite);
|
|
416
|
+ for(int i = 0; i < model->rowCount(index); i++)
|
|
417
|
+ {
|
|
418
|
+ QModelIndex channelIndex = model->index(i, 0, index);
|
|
419
|
+ QDomElement channelReferenceElement =
|
|
420
|
+ model->referenceElement(model->data(channelIndex, Qt::UserRole).toString());
|
|
421
|
+ QDomNodeList channelConfigData =
|
|
422
|
+ channelReferenceElement.elementsByTagName("attribute");
|
|
423
|
+ QDomElement channelNode;
|
|
424
|
+ QVariantMap channelAttributes;
|
|
425
|
+ for(int j = 0; j < channelConfigData.size(); j++)
|
|
426
|
+ {
|
|
427
|
+ channelNode = channelConfigData.at(j).toElement();
|
|
428
|
+ channelAttributes.insert(channelNode.attribute("name"),
|
|
429
|
+ channelNode.attribute("value"));
|
|
430
|
+ }
|
|
431
|
+ ModbusScanItem scanItem;
|
|
432
|
+ QString format = channelAttributes.value("format").toString();
|
|
433
|
+ if(format == "16fixedint")
|
|
434
|
+ {
|
|
435
|
+ scanItem.format = Int16;
|
|
436
|
+ }
|
|
437
|
+ else if(format == "32floathl")
|
|
438
|
+ {
|
|
439
|
+ scanItem.format = FloatHL;
|
|
440
|
+ }
|
|
441
|
+ else if(format == "32floatlh")
|
|
442
|
+ {
|
|
443
|
+ scanItem.format = FloatLH;
|
|
444
|
+ }
|
|
445
|
+ scanItem.request.append((char)channelAttributes.value("station").toInt());
|
|
446
|
+ scanItem.request.append((char)channelAttributes.value("function").toInt());
|
|
447
|
+ quint16 startAddress = (quint16)channelAttributes.value("address").toInt();
|
|
448
|
+ char *startAddressBytes = (char*)&startAddress;
|
|
449
|
+ scanItem.request.append(startAddressBytes[1]);
|
|
450
|
+ scanItem.request.append(startAddressBytes[0]);
|
|
451
|
+ scanItem.request.append((char)0x00);
|
|
452
|
+ if(scanItem.format == Int16)
|
|
453
|
+ {
|
|
454
|
+ scanItem.request.append((char)0x01);
|
|
455
|
+ }
|
|
456
|
+ else
|
|
457
|
+ {
|
|
458
|
+ scanItem.request.append((char)0x02);
|
|
459
|
+ }
|
|
460
|
+ quint16 crc = calculateCRC(scanItem.request);
|
|
461
|
+ char *crcBytes = (char*)&crc;
|
|
462
|
+ scanItem.request.append(crcBytes[0]);
|
|
463
|
+ scanItem.request.append(crcBytes[1]);
|
|
464
|
+ scanItem.decimalPosition = channelAttributes.value("decimals").toInt();
|
|
465
|
+ if(channelAttributes.value("unit").toString() == "C")
|
|
466
|
+ {
|
|
467
|
+ scanItem.unit = Units::Celsius;
|
|
468
|
+ }
|
|
469
|
+ else
|
|
470
|
+ {
|
|
471
|
+ scanItem.unit = Units::Fahrenheit;
|
|
472
|
+ }
|
|
473
|
+ scanList.append(scanItem);
|
|
474
|
+ channels.append(new Channel);
|
|
475
|
+ channelNames.append(channelAttributes.value("column").toString());
|
|
476
|
+ hiddenStates.append(
|
|
477
|
+ channelAttributes.value("hidden").toString() == "true" ? true : false);
|
|
478
|
+ }
|
|
479
|
+}
|
|
480
|
+
|
|
481
|
+ModbusNG::~ModbusNG()
|
|
482
|
+{
|
|
483
|
+ commTimeout->stop();
|
|
484
|
+ messageDelayTimer->stop();
|
|
485
|
+ port->close();
|
|
486
|
+}
|
|
487
|
+
|
|
488
|
+void ModbusNG::sendNextMessage()
|
|
489
|
+{
|
|
490
|
+ if(scanList.length() > 0)
|
|
491
|
+ {
|
|
492
|
+ port->write(scanList.at(scanPosition).request);
|
|
493
|
+ commTimeout->start(2000);
|
|
494
|
+ messageDelayTimer->start(delayTime);
|
|
495
|
+ }
|
|
496
|
+}
|
|
497
|
+
|
|
498
|
+void ModbusNG::timeout()
|
|
499
|
+{
|
|
500
|
+ qDebug() << "Communications timeout.";
|
|
501
|
+ messageDelayTimer->start();
|
|
502
|
+}
|
|
503
|
+
|
|
504
|
+void ModbusNG::dataAvailable()
|
|
505
|
+{
|
|
506
|
+ if(messageDelayTimer->isActive())
|
|
507
|
+ {
|
|
508
|
+ messageDelayTimer->stop();
|
|
509
|
+ }
|
|
510
|
+ responseBuffer.append(port->readAll());
|
|
511
|
+ if(responseBuffer.size() < 5)
|
|
512
|
+ {
|
|
513
|
+ return;
|
|
514
|
+ }
|
|
515
|
+ if(responseBuffer.size() < 5 + responseBuffer.at(2))
|
|
516
|
+ {
|
|
517
|
+ return;
|
|
518
|
+ }
|
|
519
|
+ responseBuffer = responseBuffer.left(5 + responseBuffer.at(2));
|
|
520
|
+ commTimeout->stop();
|
|
521
|
+ if(calculateCRC(responseBuffer) == 0)
|
|
522
|
+ {
|
|
523
|
+ qDebug() << responseBuffer;
|
|
524
|
+ quint16 intresponse;
|
|
525
|
+ float floatresponse;
|
|
526
|
+ char *ibytes = (char*)&intresponse;
|
|
527
|
+ char *fbytes = (char*)&floatresponse;
|
|
528
|
+ double output;
|
|
529
|
+ switch(scanList.at(scanPosition).format)
|
|
530
|
+ {
|
|
531
|
+ case Int16:
|
|
532
|
+ ibytes[0] = responseBuffer.at(4);
|
|
533
|
+ ibytes[1] = responseBuffer.at(3);
|
|
534
|
+ output = intresponse;
|
|
535
|
+ for(int i = 0; i < scanList.at(scanPosition).decimalPosition; i++)
|
|
536
|
+ {
|
|
537
|
+ output /= 10;
|
|
538
|
+ }
|
|
539
|
+ break;
|
|
540
|
+ case FloatHL:
|
|
541
|
+ fbytes[0] = responseBuffer.at(4);
|
|
542
|
+ fbytes[1] = responseBuffer.at(3);
|
|
543
|
+ fbytes[2] = responseBuffer.at(6);
|
|
544
|
+ fbytes[3] = responseBuffer.at(5);
|
|
545
|
+ output = floatresponse;
|
|
546
|
+ break;
|
|
547
|
+ case FloatLH:
|
|
548
|
+ fbytes[0] = responseBuffer.at(6);
|
|
549
|
+ fbytes[1] = responseBuffer.at(5);
|
|
550
|
+ fbytes[2] = responseBuffer.at(4);
|
|
551
|
+ fbytes[3] = responseBuffer.at(3);
|
|
552
|
+ output = floatresponse;
|
|
553
|
+ break;
|
|
554
|
+ }
|
|
555
|
+ if(scanList.at(scanPosition).unit == Units::Celsius)
|
|
556
|
+ {
|
|
557
|
+ output = output * 9.0 / 5.0 + 32.0;
|
|
558
|
+ }
|
|
559
|
+ scanList.at(scanPosition).lastValue = output;
|
|
560
|
+ }
|
|
561
|
+ else
|
|
562
|
+ {
|
|
563
|
+ qDebug() << "CRC failed";
|
|
564
|
+ }
|
|
565
|
+ scanPosition = (scanPosition + 1) % scanList.size();
|
|
566
|
+ if(scanPosition == 0)
|
|
567
|
+ {
|
|
568
|
+ QTime time = QTime::currentTime();
|
|
569
|
+ for(int i = 0; i < scanList.size(); i++)
|
|
570
|
+ {
|
|
571
|
+ channels.at(i)->input(Measurement(scanList.at(i).lastValue, time, Units::Fahrenheit));
|
|
572
|
+ }
|
|
573
|
+ }
|
|
574
|
+ responseBuffer.clear();
|
|
575
|
+ messageDelayTimer->start(delayTime);
|
|
576
|
+}
|
|
577
|
+
|
|
578
|
+quint16 ModbusNG::calculateCRC(QByteArray data)
|
|
579
|
+{
|
|
580
|
+ quint16 retval = 0xFFFF;
|
|
581
|
+ int i = 0;
|
|
582
|
+ while(i < data.size())
|
|
583
|
+ {
|
|
584
|
+ retval ^= 0x00FF & (quint16)data.at(i);
|
|
585
|
+ for(int j = 0; j < 8; j++)
|
|
586
|
+ {
|
|
587
|
+ if(retval & 1)
|
|
588
|
+ {
|
|
589
|
+ retval = (retval >> 1) ^ 0xA001;
|
|
590
|
+ }
|
|
591
|
+ else
|
|
592
|
+ {
|
|
593
|
+ retval >>= 1;
|
|
594
|
+ }
|
|
595
|
+ }
|
|
596
|
+ i++;
|
|
597
|
+ }
|
|
598
|
+ return retval;
|
|
599
|
+}
|