Я пытался заставить работать функцию decodeCOBSStream COBS 0.2.0. Я отправляю данные по Bluetooth-соединению ПК из терминальной программы Hercules (значения HEX с отключенным CR/LF) на модуль Bluetooth HC-05, подключенный к порту USB через кабель FTDI. flutter_libserialport отлично справляется с необработанными значениями, я могу вывести их на консоль отладки, и все выглядит нормально.
Однако когда я пытаюсь передать поток в decodeCOBSStream и распечатать его, первое сообщение печатается нормально. Иногда это делает второй. Затем он перестает печатать сообщения, и в конечном итоге процедура чтения flutter_libserialport выдает исключение в операторе возврата.
ArgumentError (Недопустимый аргумент(ы): длина должна находиться в диапазоне [0, 4611686018427387903].)
Это рассматриваемая функция из flutter_libserialport:
static Uint8List read(int bytes, UtilFunc<ffi.Uint8> readFunc) {
return ffi.using((arena) {
final ptr = arena<ffi.Uint8>(bytes);
final len = call(() => readFunc(ptr));
return Uint8List.fromList(ptr.asTypedList(len));
});
}
Вот мой код. Если я создам список, который отправляет значения, поступающие из предстоящих данных, в onDataReceived (изменив его на использование Uint8List в качестве входных данных), все в порядке. Я почти уверен, что именно так я помещаю decodeCOBSStream между ними. flutter_libserialport работает с Uint8List, а decodeCOBSStream работает с ByteData.
import 'dart:typed_data';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_libserialport/flutter_libserialport.dart';
import 'package:cobs2/cobs2.dart';
const kDebugMode = true;
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
List<String> availablePort = SerialPort.availablePorts;
SerialPort port = SerialPort('COM4');
SerialPortReader reader = SerialPortReader(port);
final controller = StreamController<ByteData>();
debugPrint('Available Ports: $availablePort');
/*
Stream<ByteData> upcomingData = reader.stream.map((data) {
ByteData byteData = data.buffer.asByteData();
return byteData;
});
*/
// Received the serial data from the SerialPortReader 'reader'
Stream<Uint8List> upcomingData = reader.stream.map((data) {
return data;
});
controller.addStream(decodeCOBSStream(upcomingData.byteData));
// When data is received
void onDataReceived(ByteData data) {
var decodeUint8 = data.buffer.asUint8List();
var decodedPrt =
getHexString(decodeUint8, offset: 0, length: decodeUint8.length);
debugPrint('Decoded: $decodedPrt');
}
/* // When data is received
void onDataReceived(Uint8List data) {
var decodedPrt = getHexString(data, offset: 0, length: data.length);
debugPrint('Decoded: $decodedPrt');
}
*/
try {
port.openReadWrite();
initSerial(port);
// Send data to serial port
serialSend(port, 'Ready for data!');
// Creates a listener to receive the decoded data
//controller.stream.listen((data) => onDataReceived(data));
controller.stream.listen(onDataReceived).onDone(() {});
//upcomingData.listen(onDataReceived).onDone(() {});
} on SerialPortError catch (err, _) {
debugPrint('${SerialPort.lastError}');
port.close();
}
return Container();
}
}
extension on Stream<Uint8List> {
Stream<ByteData> get byteData => transform(Uint8ListToByteData());
}
// Stream transformer that converts Uint8List data to ByteData.
class Uint8ListToByteData extends StreamTransformerBase<Uint8List, ByteData> {
@override
Stream<ByteData> bind(Stream<Uint8List> stream) {
return stream.map((data) => data.buffer.asByteData());
}
}
Uint8List _stringToUint8List(String data) {
List<int> codeUnits = data.codeUnits;
Uint8List uint8list = Uint8List.fromList(codeUnits);
return uint8list;
}
// Converts a String of multiple integers into HEX
String getHexString(
List<int> list, {
required int offset,
required int length,
}) {
var sublist = list.getRange(offset, offset + length);
return [
for (var byte in sublist)
byte.toRadixString(16).padLeft(2, '0').toUpperCase()
].join();
}
void initSerial(SerialPort port) {
var config = port.config;
config.baudRate = 9600;
config.bits = 8;
config.stopBits = 1;
config.parity = 0;
port.config = config;
debugPrint('BaudRate = 9600');
}
void serialSend(SerialPort port, String data) {
debugPrint('Written Bytes: ${port.write(_stringToUint8List(data))}');
}
Я пробовал передавать данные в flutter_libserialport разными способами. Я попытался изменить его с Uint8List на ByteData в функции предстоящих данных и попытался использовать для этого преобразователи потока. Некоторые из опробованных мной вариантов показаны, но закомментированы.
Это то, над чем я изначально работал. У меня был буфер, который я добавлял по мере получения данных. Я раньше не играл с ByteDataReader. Можете ли вы указать мне на хороший пример?
Имеет ли смысл для этого просто использовать пакет Buffer. Кажется, используется ByteDataReader.
Да, именно там это и определено. Вам нужно место, куда вы можете сбрасывать входящие необработанные байты, а затем постепенно считывать их, снимая украшения из початков.
ByteDataReader
— полезный класс для сбора байтов, которые вы будете постепенно обрабатывать. Это позволяет добавлять байты по мере их поступления по сети. У него есть еще одна функция «блокировки», если для формирования полного сообщения недостаточно байтов, то есть в вашем случае, если еще нет нулевого байтового разделителя.
Это позволяет нам взять пример кода C из Википедии и быстро реализовать метод async
, который возвращает полное сообщение, как только оно станет доступным. Обертывание этого в метод async*
дает поток таких сообщений по мере добавления байтов.
Полный рабочий пример показан ниже:
import 'dart:typed_data';
import 'package:buffer/buffer.dart';
import 'package:convert/convert.dart';
void main() async {
final reader = ByteDataReader(copy: true);
cobsDecode(reader).listen((o) => print(o));
reader.add(hex.decode('01010100'));
reader.add(hex.decode('031122023300'));
reader.add(hex.decode('031122')); // the above block split in 2
reader.add(hex.decode('023300'));
// add some more bytes later
Future.delayed(
Duration(seconds: 2),
() => reader.add(hex.decode('031122023300')),
);
}
Stream<Uint8List> cobsDecode(ByteDataReader reader) async* {
while (true) {
yield await _readBlock(reader);
}
}
Future<Uint8List> _readBlock(ByteDataReader reader) async {
final output = BytesBuilder();
var code = 0xff;
var block = 0;
while (true) {
await reader.readAhead(1); // ensure at least one byte available
if (block != 0) {
output.addByte(reader.readUint8()); // todo - add non-zero check
block--;
} else {
block = reader.readUint8();
if (block != 0 && code != 0xff) {
output.addByte(0);
}
code = block;
if (code == 0) {
break;
}
block--;
}
}
return output.toBytes();
}
Обратите внимание, что в этом простом примере нет типичной обработки ошибок, например проверки отсутствия нулей внутри блока.
Повышение эффективности, например чтение block
байт из устройства чтения за один раз, оставлено в качестве упражнения для чтения.
Спасибо! Это хороший пример! Мне нужно было переупорядочить несколько основных вещей и распечатать результат в шестнадцатеричном формате, чтобы понять его, но я получаю его (в основном фон C... поэтому я все еще думаю последовательно). Мне нужно немного поиграть с _readBlock, чтобы полностью усвоить это, прежде чем пытаться интегрировать концепции в свой код.
Как проверить отдельные элементы в ByteBuilder на наличие значения? Типа 0 в конце? Кроме того, есть ли способ удалить элементы, например, методы RemoveAt или RemoveLast для Uint8List?
Итак, ждите readAhead(1); делает так, чтобы одновременно обрабатывался только один байт? Или именно так работает ByteReader? Функция, принимающая его в качестве входных данных, получает для обработки только 1 байт за раз?
Обратите внимание, что между ByteBuilder и BytesBuilder есть разница. Я просто использовал встроенный в него Дарт. В любом случае нет возможности проверять элементы при создании байтов в построителе байтов. Вы просто их засовываете, а в конце звоните toBytes
. В этом коде я просто использую построитель как способ сохранить выходные данные при анализе входных данных. По сравнению с Wikipedia C Impl это эквивалент @param data Pointer to decoded output data
.
При декодировании меня интересует не то, что я уже создал, а только следующий входной байт. Если вы хотите манипулировать конечным результатом, подождите, пока после вызова toBytes
у вас не появится Uint8List
.
Да, я обрабатываю по одному байту от входа к выходу — точно так же, как код Википедии. (Я предполагаю, что вы не обрабатываете мегабайты данных! Поэтому нет необходимости преждевременно оптимизировать.) Коду Dart нужен какой-то способ «подождать», если полное сообщение еще не получено. В этом и заключается цель await reader.readAhead(1);
, которая гласит: подождите, пока во входных данных не будет хотя бы один байт. Фактический байт считывается из входных данных с помощью reader.readUint8()
, который считывает следующий байт из входных данных (и выдает ошибку, если входные данные пусты).
Вероятно, общий объем данных в пакете будет около 1 тыс. Скорее всего, половина этого. Ах, значит, readUint8() ограничивает количество байтов, извлекаемых за раз? Я предполагаю, что есть способ извлечь блоки байтов, чтобы оптимизировать код, как вы намекаете в этом примере.
Правильно — сравните это с read, которое считывает N байт из входных данных. Итак, если вы знаете, что хотите получить следующие 10 байт за один раз (и вы ожидаете readAhead(10), поэтому знаете, что они доступны), просто вызовите reader.read(10)
. Честно говоря, с несколькими килобайтами данных оно того не стоит. Хорошая новость в том, что у вас есть функционально правильная версия для сравнения, а также код package:cobs, который можно читать и учиться. (Кстати, на этом этапе вам следует «принять» ответ, если он был полезен.)
Да, это было очень информативно и указало мне правильное направление. Спасибо!
Код
cobs
, ну, старый и его можно улучшить. Учитывая, что вся библиотека находится в одном исходном файле и умещается примерно на 2 страницах, вы легко можете переписать ее для работы с типизированными данными вместоByteData
. Я бы посоветовал вам добавить полученные байты в ByteDataReader. Затем вы можете легко читать по одному байту за раз, находя разделитель 0x00 и обрабатывая байты между разделителями.