EmbeddedRelated.com
Forums
The 2024 Embedded Online Conference

Do you use serialization formats for communication?

Started by pozz October 19, 2016
I often have the need to exchange some data between two or more MCUs. I 
usually use I2C or UART as physical layers.

Normally I design a simple protocol between the MCUs: one framing 
mechanism (Start Of Frame, End Of Frame), one integrity check mechanism 
(CRC), and so on.

The payload is statically defined between the two MCUs:
- first byte is the version
- second byte is the voltage monitoring level
- third and fourt bytes are some flags
- ... and so on

As you can understand, both MCUs *must* know and agree about that 
protocol format. However during the lifetime of the product, I need to 
add some functionality or fix some bugs and those activites can lead to 
a review of the protocol format (maybe i need two bytes for the voltage 
level). Sometime, the two MCUs have a different version with a different 
protocol format implementation. In order to avoid protocol 
incompatibility, they all knows about the protocol formats used before, 
so they can adapt the parsing function to the real current protocol format.
As you can understand, it could be a trouble.

So I'm thinking to use a "self-descriptive" serializer protocol format, 
such as Protobuf, Message Pack, BSON and so on.

Do you use one serialization format? Which one?

Of course, it should be simple to implement (in transmission/encoding 
and reception/decoding) in a small embedded MCU in C language, without 
dynamic memory support.
On 20.10.2016 г. 01:22, pozz wrote:
> I often have the need to exchange some data between two or more MCUs. I > usually use I2C or UART as physical layers. > > Normally I design a simple protocol between the MCUs: one framing > mechanism (Start Of Frame, End Of Frame), one integrity check mechanism > (CRC), and so on. > > The payload is statically defined between the two MCUs: > - first byte is the version > - second byte is the voltage monitoring level > - third and fourt bytes are some flags > - ... and so on > > As you can understand, both MCUs *must* know and agree about that > protocol format. However during the lifetime of the product, I need to > add some functionality or fix some bugs and those activites can lead to > a review of the protocol format (maybe i need two bytes for the voltage > level). Sometime, the two MCUs have a different version with a different > protocol format implementation. In order to avoid protocol > incompatibility, they all knows about the protocol formats used before, > so they can adapt the parsing function to the real current protocol format. > As you can understand, it could be a trouble. > > So I'm thinking to use a "self-descriptive" serializer protocol format, > such as Protobuf, Message Pack, BSON and so on. > > Do you use one serialization format? Which one? > > Of course, it should be simple to implement (in transmission/encoding > and reception/decoding) in a small embedded MCU in C language, without > dynamic memory support.
I think you can just use PPP. Simple enough, easy to implement, proven to be robust, tested over a very long time. And of course it can be handy if at some point you want to put IP through it. It's been 10+ years since I last implemented it so my memory of it is somewhat blurry but I remember it did not take me too long. Not that I understand why the need for encapsulation etc. but since you want it this is the proven way to go. Dimiter ------------------------------------------------------ Dimiter Popoff, TGI http://www.tgi-sci.com ------------------------------------------------------ http://www.flickr.com/photos/didi_tgi/
On 2016-10-19, Dimiter_Popoff <dp@tgi-sci.com> wrote:
> On 20.10.2016 &#1075;. 01:22, pozz wrote: >> >> As you can understand, both MCUs *must* know and agree about that >> protocol format. However during the lifetime of the product, I need to >> add some functionality or fix some bugs and those activites can lead to >> a review of the protocol format (maybe i need two bytes for the voltage >> level). Sometime, the two MCUs have a different version with a different >> protocol format implementation. In order to avoid protocol >> incompatibility, they all knows about the protocol formats used before, >> so they can adapt the parsing function to the real current protocol format. >> As you can understand, it could be a trouble. >> >> So I'm thinking to use a "self-descriptive" serializer protocol format, >> such as Protobuf, Message Pack, BSON and so on. >> >> Do you use one serialization format? Which one? >> >> Of course, it should be simple to implement (in transmission/encoding >> and reception/decoding) in a small embedded MCU in C language, without >> dynamic memory support. > > I think you can just use PPP. Simple enough, easy to implement, > proven to be robust, tested over a very long time. And of course it > can be handy if at some point you want to put IP through it. > It's been 10+ years since I last implemented it so my memory of it > is somewhat blurry but I remember it did not take me too long. >
PPP doesn't help you when the message format itself changes and you need to maintain backwards compatibility which is what pozz seems to be worried about. I wonder if a solution might be a Kermit style exchange of attributes in an attributes packet and maybe with the attributes describing the individual fields in addition to the implemented protocol capabilities. In other words, maybe do an exchange of capabilities supported by the peer instead of just a version number. One of the key things about the Kermit attributes packet is that it is variable length and if the attributes packet isn't big enough to describe a specific capability then the code knows that this specific Kermit protocol extension isn't supported by the peer. Simon. -- Simon Clubley, clubley@remove_me.eisner.decus.org-Earth.UFP Microsoft: Bringing you 1980s technology to a 21st century world
On 10/19/2016 3:22 PM, pozz wrote:
> I often have the need to exchange some data between two or more MCUs. I usually > use I2C or UART as physical layers. > > Normally I design a simple protocol between the MCUs: one framing mechanism > (Start Of Frame, End Of Frame), one integrity check mechanism (CRC), and so on. > > The payload is statically defined between the two MCUs: > - first byte is the version > - second byte is the voltage monitoring level > - third and fourt bytes are some flags > - ... and so on > > As you can understand, both MCUs *must* know and agree about that protocol > format. However during the lifetime of the product, I need to add some > functionality or fix some bugs and those activites can lead to a review of the > protocol format (maybe i need two bytes for the voltage level). Sometime, the > two MCUs have a different version with a different protocol format > implementation. In order to avoid protocol incompatibility, they all knows > about the protocol formats used before, so they can adapt the parsing function > to the real current protocol format. > As you can understand, it could be a trouble. > > So I'm thinking to use a "self-descriptive" serializer protocol format, such as > Protobuf, Message Pack, BSON and so on. > > Do you use one serialization format? Which one? > > Of course, it should be simple to implement (in transmission/encoding and > reception/decoding) in a small embedded MCU in C language, without dynamic > memory support.
Don't rely on positional information in the protocol to convey information. Instead, *tag* each "value" and then just keep track of how to decode each particular tag. If you encounter a tag that you don't understand, then you have to have designed the protocol such that the 'default' for that tagged value is acceptable. If you change the *encoding* of a particular 'value', then you have to create a new, unique tag for that value in the new encoding. Note that you can also include the value in its original encoding -- under the original 'tag' -- in with the *new* encoding (and its new tag). Look at protocols like DHCP, BOOTP, etc. to see how others have been doing this (in a future/past-safe manner) for decades...
On 20/10/16 00:22, pozz wrote:
> I often have the need to exchange some data between two or more MCUs. I > usually use I2C or UART as physical layers. > > Normally I design a simple protocol between the MCUs: one framing > mechanism (Start Of Frame, End Of Frame), one integrity check mechanism > (CRC), and so on. > > The payload is statically defined between the two MCUs: > - first byte is the version > - second byte is the voltage monitoring level > - third and fourt bytes are some flags > - ... and so on > > As you can understand, both MCUs *must* know and agree about that > protocol format. However during the lifetime of the product, I need to > add some functionality or fix some bugs and those activites can lead to > a review of the protocol format (maybe i need two bytes for the voltage > level). Sometime, the two MCUs have a different version with a different > protocol format implementation. In order to avoid protocol > incompatibility, they all knows about the protocol formats used before, > so they can adapt the parsing function to the real current protocol format. > As you can understand, it could be a trouble. > > So I'm thinking to use a "self-descriptive" serializer protocol format, > such as Protobuf, Message Pack, BSON and so on. > > Do you use one serialization format? Which one? > > Of course, it should be simple to implement (in transmission/encoding > and reception/decoding) in a small embedded MCU in C language, without > dynamic memory support.
It depends on how flexible you want to be. Self-descriptive or tagged formats, like JSON, BSON, etc., are very future-proof - but they are also much more effort in development time and run time. You can come a /long/ way with just a little more than the system you have. Keep the same framing mechanism, but make sure you have a field for "length of payload". In the payload, you have "type of telegram" and "version of telegram format". Then when you need to change the formats, you add new data to the old structure. So format version 1 might be: typedef struct { uint8_t programVersion; uint8_t voltageMonitor; uint16_t flags; } format1payload; static_assert(sizeof(format1payload) == 4); Format version 2, with voltage now in millivolts, will be: typedef struct { uint8_t programVersion; uint8_t voltageMonitor; uint16_t flags; // Start of version 2 uint16_t voltageMonitorMillivolts; } format2payload; static_assert(sizeof(format2payload) == 6); A transmitter always sends with the latest version it knows, and will fill in both the voltageMonitor and voltageMonitorMillivolts fields. A receiver interprets as much as it can based on the latest version it knows and the version it receives - any excess data beyond its understanding can safely be ignored. Your encoder and decoders are now nothing more than casts between char* pointers and struct pointers.
A simple and quite robust message format which is also simple to parse:

START-OF-FRAME
FRAME-LENGTH
FRAME-LENGTH-INV
FRAME-PAYLOAD
FRAME-CHECKSUM
END-OF-FRAME

The FRAME-LENGTH tells how many bytes to read in order to get the frame payload and the checksum: FRAME-LENGTH = numer-of-bytes(FRAME-PAYLOAD) + number-of-bytes(FRAME-CHECKSUM), for example. The FRAME-LENGTH-INV is 1's complement of FRAME-LENGTH and it is used for fast detection of invalid frames: If the FRAME-LENGTH is not equal to ones-complement-of(FRAME_LENGTH-INV) the frame needs to be discarded as the frame is already corrupted.

The FRAME-PAYLOAD is a sequence on PAYLOAD-ITEMs:

PAYLOAD-ITEM:
ITEM-ID
ITEM-LENGTH
ITEM-PAYLOAD.

The ITEM-ID tells what the data item this is, the ITEM-LENGTH tells the length of the item payload and the ITEM-PAYLOAD contains the byte information for the item.

Pretty simple message format and easy to parse. If the parser doesn't recognize the item id, it knows how many bytes to skip for the next item.

Br,
Kalvin
Il 20/10/2016 09:40, David Brown ha scritto:
> On 20/10/16 00:22, pozz wrote: >> I often have the need to exchange some data between two or more MCUs. I >> usually use I2C or UART as physical layers. >> >> Normally I design a simple protocol between the MCUs: one framing >> mechanism (Start Of Frame, End Of Frame), one integrity check mechanism >> (CRC), and so on. >> >> The payload is statically defined between the two MCUs: >> - first byte is the version >> - second byte is the voltage monitoring level >> - third and fourt bytes are some flags >> - ... and so on >> >> As you can understand, both MCUs *must* know and agree about that >> protocol format. However during the lifetime of the product, I need to >> add some functionality or fix some bugs and those activites can lead to >> a review of the protocol format (maybe i need two bytes for the voltage >> level). Sometime, the two MCUs have a different version with a different >> protocol format implementation. In order to avoid protocol >> incompatibility, they all knows about the protocol formats used before, >> so they can adapt the parsing function to the real current protocol format. >> As you can understand, it could be a trouble. >> >> So I'm thinking to use a "self-descriptive" serializer protocol format, >> such as Protobuf, Message Pack, BSON and so on. >> >> Do you use one serialization format? Which one? >> >> Of course, it should be simple to implement (in transmission/encoding >> and reception/decoding) in a small embedded MCU in C language, without >> dynamic memory support. > > It depends on how flexible you want to be. Self-descriptive or tagged > formats, like JSON, BSON, etc., are very future-proof - but they are > also much more effort in development time and run time. > > You can come a /long/ way with just a little more than the system you > have. Keep the same framing mechanism, but make sure you have a field > for "length of payload". In the payload, you have "type of telegram" > and "version of telegram format". Then when you need to change the > formats, you add new data to the old structure. > > So format version 1 might be: > > typedef struct { > uint8_t programVersion; > uint8_t voltageMonitor; > uint16_t flags; > } format1payload; > static_assert(sizeof(format1payload) == 4); > > Format version 2, with voltage now in millivolts, will be: > > typedef struct { > uint8_t programVersion; > uint8_t voltageMonitor; > uint16_t flags; > // Start of version 2 > uint16_t voltageMonitorMillivolts; > } format2payload; > static_assert(sizeof(format2payload) == 6); > > A transmitter always sends with the latest version it knows, and will > fill in both the voltageMonitor and voltageMonitorMillivolts fields. A > receiver interprets as much as it can based on the latest version it > knows and the version it receives - any excess data beyond its > understanding can safely be ignored. > > Your encoder and decoders are now nothing more than casts between char* > pointers and struct pointers.
So you use cast your struct pointers to char pointers and send it as is? I used this very simple technique in the past, but I don't use it anymore. Because the two MCUs could be different, could use a different endianness, could use a different compiler that places padding in different places, and so on.
On 20.10.16 14:52, pozz wrote:
> Il 20/10/2016 09:40, David Brown ha scritto: >> On 20/10/16 00:22, pozz wrote: >>> I often have the need to exchange some data between two or more MCUs. I >>> usually use I2C or UART as physical layers. >>> >>> Normally I design a simple protocol between the MCUs: one framing >>> mechanism (Start Of Frame, End Of Frame), one integrity check mechanism >>> (CRC), and so on. >>> >>> The payload is statically defined between the two MCUs: >>> - first byte is the version >>> - second byte is the voltage monitoring level >>> - third and fourt bytes are some flags >>> - ... and so on >>> >>> As you can understand, both MCUs *must* know and agree about that >>> protocol format. However during the lifetime of the product, I need to >>> add some functionality or fix some bugs and those activites can lead to >>> a review of the protocol format (maybe i need two bytes for the voltage >>> level). Sometime, the two MCUs have a different version with a different >>> protocol format implementation. In order to avoid protocol >>> incompatibility, they all knows about the protocol formats used before, >>> so they can adapt the parsing function to the real current protocol >>> format. >>> As you can understand, it could be a trouble. >>> >>> So I'm thinking to use a "self-descriptive" serializer protocol format, >>> such as Protobuf, Message Pack, BSON and so on. >>> >>> Do you use one serialization format? Which one? >>> >>> Of course, it should be simple to implement (in transmission/encoding >>> and reception/decoding) in a small embedded MCU in C language, without >>> dynamic memory support. >> >> It depends on how flexible you want to be. Self-descriptive or tagged >> formats, like JSON, BSON, etc., are very future-proof - but they are >> also much more effort in development time and run time. >> >> You can come a /long/ way with just a little more than the system you >> have. Keep the same framing mechanism, but make sure you have a field >> for "length of payload". In the payload, you have "type of telegram" >> and "version of telegram format". Then when you need to change the >> formats, you add new data to the old structure. >> >> So format version 1 might be: >> >> typedef struct { >> uint8_t programVersion; >> uint8_t voltageMonitor; >> uint16_t flags; >> } format1payload; >> static_assert(sizeof(format1payload) == 4); >> >> Format version 2, with voltage now in millivolts, will be: >> >> typedef struct { >> uint8_t programVersion; >> uint8_t voltageMonitor; >> uint16_t flags; >> // Start of version 2 >> uint16_t voltageMonitorMillivolts; >> } format2payload; >> static_assert(sizeof(format2payload) == 6); >> >> A transmitter always sends with the latest version it knows, and will >> fill in both the voltageMonitor and voltageMonitorMillivolts fields. A >> receiver interprets as much as it can based on the latest version it >> knows and the version it receives - any excess data beyond its >> understanding can safely be ignored. >> >> Your encoder and decoders are now nothing more than casts between char* >> pointers and struct pointers. > > So you use cast your struct pointers to char pointers and send it as is? > I used this very simple technique in the past, but I don't use it > anymore. Because the two MCUs could be different, could use a different > endianness, could use a different compiler that places padding in > different places, and so on.
The canonical solution to this is to follow the Internet practices: Pack data tightly and use network byte order for multibyte binary data. This often needs byte transfers to pack and unpack the communication frames. The network byte order is in reverse for e.g. PC hardware. Another way is to transfer all data as text. There is a good reason why XML is used in this kind of situations, though it is an extremely loose format, plenty of overhead compared to data. -- -TV
On 20/10/16 13:52, pozz wrote:
> Il 20/10/2016 09:40, David Brown ha scritto: >> On 20/10/16 00:22, pozz wrote: >>> I often have the need to exchange some data between two or more MCUs. I >>> usually use I2C or UART as physical layers. >>> >>> Normally I design a simple protocol between the MCUs: one framing >>> mechanism (Start Of Frame, End Of Frame), one integrity check mechanism >>> (CRC), and so on. >>> >>> The payload is statically defined between the two MCUs: >>> - first byte is the version >>> - second byte is the voltage monitoring level >>> - third and fourt bytes are some flags >>> - ... and so on >>> >>> As you can understand, both MCUs *must* know and agree about that >>> protocol format. However during the lifetime of the product, I need to >>> add some functionality or fix some bugs and those activites can lead to >>> a review of the protocol format (maybe i need two bytes for the voltage >>> level). Sometime, the two MCUs have a different version with a different >>> protocol format implementation. In order to avoid protocol >>> incompatibility, they all knows about the protocol formats used before, >>> so they can adapt the parsing function to the real current protocol >>> format. >>> As you can understand, it could be a trouble. >>> >>> So I'm thinking to use a "self-descriptive" serializer protocol format, >>> such as Protobuf, Message Pack, BSON and so on. >>> >>> Do you use one serialization format? Which one? >>> >>> Of course, it should be simple to implement (in transmission/encoding >>> and reception/decoding) in a small embedded MCU in C language, without >>> dynamic memory support. >> >> It depends on how flexible you want to be. Self-descriptive or tagged >> formats, like JSON, BSON, etc., are very future-proof - but they are >> also much more effort in development time and run time. >> >> You can come a /long/ way with just a little more than the system you >> have. Keep the same framing mechanism, but make sure you have a field >> for "length of payload". In the payload, you have "type of telegram" >> and "version of telegram format". Then when you need to change the >> formats, you add new data to the old structure. >> >> So format version 1 might be: >> >> typedef struct { >> uint8_t programVersion; >> uint8_t voltageMonitor; >> uint16_t flags; >> } format1payload; >> static_assert(sizeof(format1payload) == 4); >> >> Format version 2, with voltage now in millivolts, will be: >> >> typedef struct { >> uint8_t programVersion; >> uint8_t voltageMonitor; >> uint16_t flags; >> // Start of version 2 >> uint16_t voltageMonitorMillivolts; >> } format2payload; >> static_assert(sizeof(format2payload) == 6); >> >> A transmitter always sends with the latest version it knows, and will >> fill in both the voltageMonitor and voltageMonitorMillivolts fields. A >> receiver interprets as much as it can based on the latest version it >> knows and the version it receives - any excess data beyond its >> understanding can safely be ignored. >> >> Your encoder and decoders are now nothing more than casts between char* >> pointers and struct pointers. > > So you use cast your struct pointers to char pointers and send it as is? > I used this very simple technique in the past, but I don't use it > anymore. Because the two MCUs could be different, could use a different > endianness, could use a different compiler that places padding in > different places, and so on. >
It is not a problem if the MCUs are different. It would matter if they had different encodings for signed integers or padding bits in their types, but let's assume you are not communicating with a mainframe from the 60's. Padding is not a problem if you design your structs carefully. Make sure everything is naturally aligned - 16-bit data is 16-bit aligned, 32-bit data is 32-bit aligned, 64-bit data is 64-bit aligned. Use your tools to check this - "-Wpadded" for gcc, and static_asserts to check that the sizes of your structs match what you expect. That just leaves endianness. Most microcontrollers are little-endian, as are PC's, so that is the endianness I normally use. The only exception would be if I were transferring data between two big-endian devices, I would probably use big-endian ordering. So if I have a networked system with different endians on different microcontrollers, then I need to do endian swaps on the structs at one end. Some compilers support this, letting you annotate your structs with the endianness (gcc 6 has this, though I haven't tried the feature yet). Otherwise it must be done manually when receiving or transmitting the struct. But still, it is a fraction of the effort (in development time and run time) of decoding more general protocol formats.
Il 20/10/2016 15:45, David Brown ha scritto:
> On 20/10/16 13:52, pozz wrote: >> Il 20/10/2016 09:40, David Brown ha scritto: >>> On 20/10/16 00:22, pozz wrote: >>>> I often have the need to exchange some data between two or more MCUs. I >>>> usually use I2C or UART as physical layers. >>>> >>>> Normally I design a simple protocol between the MCUs: one framing >>>> mechanism (Start Of Frame, End Of Frame), one integrity check mechanism >>>> (CRC), and so on. >>>> >>>> The payload is statically defined between the two MCUs: >>>> - first byte is the version >>>> - second byte is the voltage monitoring level >>>> - third and fourt bytes are some flags >>>> - ... and so on >>>> >>>> As you can understand, both MCUs *must* know and agree about that >>>> protocol format. However during the lifetime of the product, I need to >>>> add some functionality or fix some bugs and those activites can lead to >>>> a review of the protocol format (maybe i need two bytes for the voltage >>>> level). Sometime, the two MCUs have a different version with a different >>>> protocol format implementation. In order to avoid protocol >>>> incompatibility, they all knows about the protocol formats used before, >>>> so they can adapt the parsing function to the real current protocol >>>> format. >>>> As you can understand, it could be a trouble. >>>> >>>> So I'm thinking to use a "self-descriptive" serializer protocol format, >>>> such as Protobuf, Message Pack, BSON and so on. >>>> >>>> Do you use one serialization format? Which one? >>>> >>>> Of course, it should be simple to implement (in transmission/encoding >>>> and reception/decoding) in a small embedded MCU in C language, without >>>> dynamic memory support. >>> >>> It depends on how flexible you want to be. Self-descriptive or tagged >>> formats, like JSON, BSON, etc., are very future-proof - but they are >>> also much more effort in development time and run time. >>> >>> You can come a /long/ way with just a little more than the system you >>> have. Keep the same framing mechanism, but make sure you have a field >>> for "length of payload". In the payload, you have "type of telegram" >>> and "version of telegram format". Then when you need to change the >>> formats, you add new data to the old structure. >>> >>> So format version 1 might be: >>> >>> typedef struct { >>> uint8_t programVersion; >>> uint8_t voltageMonitor; >>> uint16_t flags; >>> } format1payload; >>> static_assert(sizeof(format1payload) == 4); >>> >>> Format version 2, with voltage now in millivolts, will be: >>> >>> typedef struct { >>> uint8_t programVersion; >>> uint8_t voltageMonitor; >>> uint16_t flags; >>> // Start of version 2 >>> uint16_t voltageMonitorMillivolts; >>> } format2payload; >>> static_assert(sizeof(format2payload) == 6); >>> >>> A transmitter always sends with the latest version it knows, and will >>> fill in both the voltageMonitor and voltageMonitorMillivolts fields. A >>> receiver interprets as much as it can based on the latest version it >>> knows and the version it receives - any excess data beyond its >>> understanding can safely be ignored. >>> >>> Your encoder and decoders are now nothing more than casts between char* >>> pointers and struct pointers. >> >> So you use cast your struct pointers to char pointers and send it as is? >> I used this very simple technique in the past, but I don't use it >> anymore. Because the two MCUs could be different, could use a different >> endianness, could use a different compiler that places padding in >> different places, and so on. >> > > It is not a problem if the MCUs are different. It would matter if they > had different encodings for signed integers or padding bits in their > types, but let's assume you are not communicating with a mainframe from > the 60's. > > Padding is not a problem if you design your structs carefully. Make > sure everything is naturally aligned - 16-bit data is 16-bit aligned, > 32-bit data is 32-bit aligned, 64-bit data is 64-bit aligned. Use your > tools to check this - "-Wpadded" for gcc, and static_asserts to check > that the sizes of your structs match what you expect. > > That just leaves endianness. Most microcontrollers are little-endian, > as are PC's, so that is the endianness I normally use. The only > exception would be if I were transferring data between two big-endian > devices, I would probably use big-endian ordering. > > So if I have a networked system with different endians on different > microcontrollers, then I need to do endian swaps on the structs at one > end. Some compilers support this, letting you annotate your structs > with the endianness (gcc 6 has this, though I haven't tried the feature > yet). Otherwise it must be done manually when receiving or transmitting > the struct. But still, it is a fraction of the effort (in development > time and run time) of decoding more general protocol formats.
I knew all your arguments. As I wrote, I used in the past exactly this trick. However I don't like it. In certain cases, you have to change the order of the fields in a struct (an order that appears logical), only because you have to avoid padding bytes. Moreover, if you need to encode some complex structs, understanding if the compiler will introduce padding in-between is not trivial. send(&struct1, sizeof(struct1)); send(&struct2, sizeof(struct2)); sizeof(struct1) could consider some extra padding bytes at the end of the struct. The receiver should know about it. One time I had to communicate with a Visual Basic application. In that case, managin padding bytes was a mess.

The 2024 Embedded Online Conference