From df4e26c8757a81bb40ba2fd60431d5d1ecd64b11 Mon Sep 17 00:00:00 2001 From: Linus Torvalds Date: Sun, 17 Aug 2014 12:26:21 -0600 Subject: [PATCH] Start sanitizing gaschange event information Decode the gasmix data into a sane format when creating the event, and add the (currently unused) ability to specify a gas change to a particular cylinder rather than (or in addition to) the gasmix. Signed-off-by: Linus Torvalds Signed-off-by: Dirk Hohndel --- dive.c | 68 +++++++++++++--- dive.h | 22 +++++- load-git.c | 21 ++++- parse-xml.c | 136 +++++++++++++++++++------------- profile.c | 22 +++--- qt-ui/profile/diveeventitem.cpp | 8 +- save-git.c | 27 +++++-- save-xml.c | 27 +++++-- 8 files changed, 234 insertions(+), 97 deletions(-) diff --git a/dive.c b/dive.c index 9972212aa..e7bf685e6 100644 --- a/dive.c +++ b/dive.c @@ -26,7 +26,31 @@ static const char *default_tags[] = { QT_TRANSLATE_NOOP("gettextFromC", "deco") }; -void add_event(struct divecomputer *dc, int time, int type, int flags, int value, const char *name) +int event_is_gaschange(struct event *ev) +{ + return ev->type == SAMPLE_EVENT_GASCHANGE || + ev->type == SAMPLE_EVENT_GASCHANGE2; +} + +/* + * Does the gas mix data match the legacy + * libdivecomputer event format? If so, + * we can skip saving it, in order to maintain + * the old save formats. We'll re-generate the + * gas mix when loading. + */ +int event_gasmix_redundant(struct event *ev) +{ + int value = ev->value; + int o2, he; + + o2 = (value & 0xffff) * 10; + he = (value >> 16) * 10; + return o2 == ev->gas.mix.o2.permille && + he == ev->gas.mix.he.permille; +} + +struct event *add_event(struct divecomputer *dc, int time, int type, int flags, int value, const char *name) { struct event *ev, **p; unsigned int size, len = strlen(name); @@ -34,7 +58,7 @@ void add_event(struct divecomputer *dc, int time, int type, int flags, int value size = sizeof(*ev) + len + 1; ev = malloc(size); if (!ev) - return; + return NULL; memset(ev, 0, size); memcpy(ev->name, name, len); ev->time.seconds = time; @@ -42,6 +66,22 @@ void add_event(struct divecomputer *dc, int time, int type, int flags, int value ev->flags = flags; ev->value = value; + /* + * Expand the events into a sane format. Currently + * just gas switches + */ + switch (type) { + case SAMPLE_EVENT_GASCHANGE2: + /* High 16 bits are He percentage */ + ev->gas.mix.he.permille = (value >> 16) * 10; + /* Fallthrough */ + case SAMPLE_EVENT_GASCHANGE: + /* Low 16 bits are O2 percentage */ + ev->gas.mix.o2.permille = (value & 0xffff) * 10; + ev->gas.index = -1; + break; + } + p = &dc->events; /* insert in the sorted list of events */ @@ -50,6 +90,7 @@ void add_event(struct divecomputer *dc, int time, int type, int flags, int value ev->next = *p; *p = ev; remember_event(name); + return ev; } static int same_event(struct event *a, struct event *b) @@ -107,14 +148,11 @@ void update_event_name(struct dive *d, struct event *event, char *name) /* this returns a pointer to static variable - so use it right away after calling */ struct gasmix *get_gasmix_from_event(struct event *ev) { - static struct gasmix g; - g.o2.permille = g.he.permille = 0; - if (ev && (ev->type == SAMPLE_EVENT_GASCHANGE || ev->type == SAMPLE_EVENT_GASCHANGE2)) { - g.o2.permille = 10 * ev->value & 0xffff; - if (ev->type == SAMPLE_EVENT_GASCHANGE2) - g.he.permille = 10 * (ev->value >> 16); - } - return &g; + static struct gasmix dummy; + if (ev && event_is_gaschange(ev)) + return &ev->gas.mix; + + return &dummy; } int get_pressure_units(int mb, const char **units) @@ -1543,6 +1581,7 @@ static void add_initial_gaschange(struct dive *dive, struct divecomputer *dc) static void dc_cylinder_renumber(struct dive *dive, struct divecomputer *dc, int mapping[]) { int i; + struct event *ev; /* Did the first gas get remapped? Add gas switch event */ if (mapping[0] > 0) @@ -1559,6 +1598,15 @@ static void dc_cylinder_renumber(struct dive *dive, struct divecomputer *dc, int if (sensor >= 0) s->sensor = sensor; } + + /* Remap the gas change indexes */ + for (ev = dc->events; ev; ev = ev->next) { + if (!event_is_gaschange(ev)) + continue; + if (ev->gas.index < 0) + continue; + ev->gas.index = mapping[ev->gas.index]; + } } /* diff --git a/dive.h b/dive.h index eb8909118..55396d8fe 100644 --- a/dive.h +++ b/dive.h @@ -85,11 +85,29 @@ typedef struct struct event { struct event *next; duration_t time; - int type, flags, value; + int type; + /* This is the annoying libdivecomputer format. */ + int flags, value; + /* .. and this is our "extended" data for some event types */ + union { + /* + * Currently only for gas switch events. + * + * NOTE! The index may be -1, which means "unknown". In that + * case, the get_cylinder_index() function will give the best + * match with the cylinders in the dive based on gasmix. + */ + struct { + int index; + struct gasmix mix; + } gas; + }; bool deleted; char name[]; }; +extern int event_is_gaschange(struct event *ev); +extern int event_gasmix_redundant(struct event *ev); extern int get_pressure_units(int mb, const char **units); extern double get_depth_units(int mm, int *frac, const char **units); @@ -651,7 +669,7 @@ extern void copy_samples(struct divecomputer *s, struct divecomputer *d); extern bool is_cylinder_used(struct dive *dive, int idx); extern void fill_default_cylinder(cylinder_t *cyl); extern void add_gas_switch_event(struct dive *dive, struct divecomputer *dc, int time, int idx); -extern void add_event(struct divecomputer *dc, int time, int type, int flags, int value, const char *name); +extern struct event *add_event(struct divecomputer *dc, int time, int type, int flags, int value, const char *name); extern void remove_event(struct event *event); extern void update_event_name(struct dive *d, struct event* event, char *name); extern void per_cylinder_mean_depth(struct dive *dive, struct divecomputer *dc, int *mean, int *duration); diff --git a/load-git.c b/load-git.c index e61a0ca13..16aa1958f 100644 --- a/load-git.c +++ b/load-git.c @@ -529,6 +529,13 @@ static void parse_event_keyvalue(void *_event, const char *key, const char *valu event->value = val; } else if (!strcmp(key, "name")) { /* We get the name from the string handling */ + } else if (!strcmp(key, "cylinder")) { + /* NOTE! We add one here as a marker that "yes, we got a cylinder index" */ + event->gas.index = 1+get_index(value); + } else if (!strcmp(key, "o2")) { + event->gas.mix.o2 = get_fraction(value); + } else if (!strcmp(key, "he")) { + event->gas.mix.he = get_fraction(value); } else report_error("Unexpected event key/value pair (%s/%s)", key, value); } @@ -538,7 +545,7 @@ static void parse_dc_event(char *line, struct membuffer *str, void *_dc) int m, s = 0; const char *name; struct divecomputer *dc = _dc; - struct event event = { 0 }; + struct event event = { 0 }, *ev; m = strtol(line, &line, 10); if (*line == ':') @@ -557,7 +564,17 @@ static void parse_dc_event(char *line, struct membuffer *str, void *_dc) name = ""; if (str->len) name = mb_cstring(str); - add_event(dc, event.time.seconds, event.type, event.flags, event.value, name); + ev = add_event(dc, event.time.seconds, event.type, event.flags, event.value, name); + if (ev && event_is_gaschange(ev)) { + /* + * We subtract one here because "0" is "no index", + * and the parsing will add one for actual cylinder + * index data (see parse_event_keyvalue) + */ + ev->gas.index = event.gas.index-1; + if (event.gas.mix.o2.permille || event.gas.mix.he.permille) + ev->gas.mix = event.gas.mix; + } } static void parse_trip_date(char *line, struct membuffer *str, void *_trip) diff --git a/parse-xml.c b/parse-xml.c index 6e74e8aaf..e88c9b944 100644 --- a/parse-xml.c +++ b/parse-xml.c @@ -95,17 +95,17 @@ const struct units IMPERIAL_units = IMPERIAL_UNITS; /* * Dive info as it is being built up.. */ +#define MAX_EVENT_NAME 128 static struct divecomputer *cur_dc; static struct dive *cur_dive; static dive_trip_t *cur_trip = NULL; static struct sample *cur_sample; static struct picture *cur_picture; -static struct { - int active; - duration_t time; - int type, flags, value; - const char *name; -} cur_event; +static union { + struct event event; + char allocation[sizeof(struct event)+MAX_EVENT_NAME]; +} event_allocation = { .event.deleted = 1 }; +#define cur_event event_allocation.event static struct { struct { const char *model; @@ -527,6 +527,15 @@ static void utf8_string(char *buffer, void *_res) *res = strdup(buffer); } +static void event_name(char *buffer, char *name) +{ + int size = trimspace(buffer); + if (size >= MAX_EVENT_NAME) + size = MAX_EVENT_NAME-1; + memcpy(name, buffer, size); + name[size] = 0; +} + /* Extract the dive computer type from the xml text buffer */ static void get_dc_type(char *buffer, enum dive_comp_type *i) { @@ -704,16 +713,22 @@ static void try_to_match_autogroup(const char *name, char *buf) void add_gas_switch_event(struct dive *dive, struct divecomputer *dc, int seconds, int idx) { - /* The gas switch event format is insane. It will be fixed, I think */ - int o2 = get_o2(&dive->cylinder[idx].gasmix); - int he = get_he(&dive->cylinder[idx].gasmix); + /* The gas switch event format is insane for historical reasons */ + struct gasmix *mix = &dive->cylinder[idx].gasmix; + int o2 = get_o2(mix); + int he = get_he(mix); + struct event *ev; int value; o2 = (o2 + 5) / 10; he = (he + 5) / 10; value = o2 + (he << 16); - add_event(dc, seconds, he ? SAMPLE_EVENT_GASCHANGE2 : SAMPLE_EVENT_GASCHANGE, 0, value, "gaschange"); + ev = add_event(dc, seconds, he ? SAMPLE_EVENT_GASCHANGE2 : SAMPLE_EVENT_GASCHANGE, 0, value, "gaschange"); + if (ev) { + ev->gas.index = idx; + ev->gas.mix = *mix; + } } static void get_cylinderindex(char *buffer, uint8_t *i) @@ -751,9 +766,9 @@ static void try_to_fill_dc_settings(const char *name, char *buf) static void try_to_fill_event(const char *name, char *buf) { start_match("event", name, buf); - if (MATCH("event", utf8_string, &cur_event.name)) + if (MATCH("event", event_name, cur_event.name)) return; - if (MATCH("name", utf8_string, &cur_event.name)) + if (MATCH("name", event_name, cur_event.name)) return; if (MATCH("time", eventtime, &cur_event.time)) return; @@ -763,6 +778,15 @@ static void try_to_fill_event(const char *name, char *buf) return; if (MATCH("value", get_index, &cur_event.value)) return; + if (MATCH("cylinder", get_index, &cur_event.gas.index)) { + /* We add one to indicate that we got an actual cylinder index value */ + cur_event.gas.index++; + return; + } + if (MATCH("o2", percent, &cur_event.gas.mix.o2)) + return; + if (MATCH("he", percent, &cur_event.gas.mix.he)) + return; nonmatch("event", name, buf); } @@ -1332,36 +1356,38 @@ static void trip_end(void) static void event_start(void) { memset(&cur_event, 0, sizeof(cur_event)); - cur_event.active = 1; + cur_event.deleted = 0; /* Active */ } static void event_end(void) { struct divecomputer *dc = get_dc(); - if (cur_event.name) { - if (strcmp(cur_event.name, "surface") != 0) { - /* 123 is a magic event that we used for a while to encode images in dives */ - if (cur_event.type == 123) { - struct picture *pic = alloc_picture(); - pic->filename = strdup(cur_event.name); - /* theoretically this could fail - but we didn't support multi year offsets */ - pic->offset.seconds = cur_event.time.seconds; - dive_add_picture(cur_dive, pic); - } else { - /* At some point gas change events did not have any type. Thus we need to add - * one on import, if we encounter the type one missing. - */ - if (cur_event.type == 0 && strcmp(cur_event.name, "gaschange") == 0) - cur_event.type = cur_event.value >> 16 > 0 ? SAMPLE_EVENT_GASCHANGE2 : SAMPLE_EVENT_GASCHANGE; - - add_event(dc, cur_event.time.seconds, - cur_event.type, cur_event.flags, - cur_event.value, cur_event.name); + if (strcmp(cur_event.name, "surface") != 0) { /* 123 is a magic event that we used for a while to encode images in dives */ + if (cur_event.type == 123) { + struct picture *pic = alloc_picture(); + pic->filename = strdup(cur_event.name); + /* theoretically this could fail - but we didn't support multi year offsets */ + pic->offset.seconds = cur_event.time.seconds; + dive_add_picture(cur_dive, pic); + } else { + struct event *ev; + /* At some point gas change events did not have any type. Thus we need to add + * one on import, if we encounter the type one missing. + */ + if (cur_event.type == 0 && strcmp(cur_event.name, "gaschange") == 0) + cur_event.type = cur_event.value >> 16 > 0 ? SAMPLE_EVENT_GASCHANGE2 : SAMPLE_EVENT_GASCHANGE; + ev = add_event(dc, cur_event.time.seconds, + cur_event.type, cur_event.flags, + cur_event.value, cur_event.name); + if (ev && event_is_gaschange(ev)) { + /* See try_to_fill_event() on why the filled-in index is one too big */ + ev->gas.index = cur_event.gas.index-1; + if (cur_event.gas.mix.o2.permille || cur_event.gas.mix.he.permille) + ev->gas.mix = cur_event.gas.mix; } } - free((void *)cur_event.name); } - cur_event.active = 0; + cur_event.deleted = 1; /* No longer active */ } static void picture_start(void) @@ -1472,7 +1498,7 @@ static void entry(const char *name, char *buf) try_to_match_autogroup(name, buf); return; } - if (cur_event.active) { + if (!cur_event.deleted) { try_to_fill_event(name, buf); return; } @@ -1722,71 +1748,71 @@ extern int dm4_events(void *handle, int columns, char **data, char **column) switch (atoi(data[2])) { case 1: /* 1 Mandatory Safety Stop */ - cur_event.name = strdup("safety stop (mandatory)"); + strcpy(cur_event.name, "safety stop (mandatory)"); break; case 3: /* 3 Deco */ /* What is Subsurface's term for going to * deco? */ - cur_event.name = strdup("deco"); + strcpy(cur_event.name, "deco"); break; case 4: /* 4 Ascent warning */ - cur_event.name = strdup("ascent"); + strcpy(cur_event.name, "ascent"); break; case 5: /* 5 Ceiling broken */ - cur_event.name = strdup("violation"); + strcpy(cur_event.name, "violation"); break; case 6: /* 6 Mandatory safety stop ceiling error */ - cur_event.name = strdup("violation"); + strcpy(cur_event.name, "violation"); break; case 7: /* 7 Below deco floor */ - cur_event.name = strdup("below floor"); + strcpy(cur_event.name, "below floor"); break; case 8: /* 8 Dive time alarm */ - cur_event.name = strdup("divetime"); + strcpy(cur_event.name, "divetime"); break; case 9: /* 9 Depth alarm */ - cur_event.name = strdup("maxdepth"); + strcpy(cur_event.name, "maxdepth"); break; case 10: /* 10 OLF 80% */ case 11: /* 11 OLF 100% */ - cur_event.name = strdup("OLF"); + strcpy(cur_event.name, "OLF"); break; case 12: /* 12 High pO₂ */ - cur_event.name = strdup("PO2"); + strcpy(cur_event.name, "PO2"); break; case 13: /* 13 Air time */ - cur_event.name = strdup("airtime"); + strcpy(cur_event.name, "airtime"); break; case 17: /* 17 Ascent warning */ - cur_event.name = strdup("ascent"); + strcpy(cur_event.name, "ascent"); break; case 18: /* 18 Ceiling error */ - cur_event.name = strdup("ceiling"); + strcpy(cur_event.name, "ceiling"); break; case 19: /* 19 Surfaced */ - cur_event.name = strdup("surface"); + strcpy(cur_event.name, "surface"); break; case 20: /* 20 Deco */ - cur_event.name = strdup("deco"); + strcpy(cur_event.name, "deco"); break; case 22: /* 22 Mandatory safety stop violation */ - cur_event.name = strdup("violation"); + strcpy(cur_event.name, "violation"); break; case 257: /* 257 Dive active */ @@ -1796,14 +1822,14 @@ extern int dm4_events(void *handle, int columns, char **data, char **column) case 258: /* 258 Bookmark */ if (data[3]) { - cur_event.name = strdup("heading"); + strcpy(cur_event.name, "heading"); cur_event.value = atoi(data[3]); } else { - cur_event.name = strdup("bookmark"); + strcpy(cur_event.name, "bookmark"); } break; default: - cur_event.name = strdup("unknown"); + strcpy(cur_event.name, "unknown"); cur_event.value = atoi(data[2]); break; } @@ -1986,7 +2012,7 @@ extern int shearwater_changes(void *handle, int columns, char **data, char **col if (data[0]) cur_event.time.seconds = atoi(data[0]); if (data[1]) { - cur_event.name = strdup("gaschange"); + strcpy(cur_event.name, "gaschange"); cur_event.value = atof(data[1]) * 100; } event_end(); diff --git a/profile.c b/profile.c index 71a9164e6..79917d6e6 100644 --- a/profile.c +++ b/profile.c @@ -289,6 +289,14 @@ struct plot_info *analyze_plot_info(struct plot_info *pi) return pi; } +/* + * If the event has an explicit cylinder index, + * we return that. If it doesn't, we return the best + * match based on the gasmix. + * + * Some dive computers give cylinder indexes, some + * give just the gas mix. + */ int get_cylinder_index(struct dive *dive, struct event *ev) { int i; @@ -296,10 +304,9 @@ int get_cylinder_index(struct dive *dive, struct event *ev) int target_o2, target_he; struct gasmix *g; - /* - * Crazy gas change events give us odd encoded o2/he in percent. - * Decode into our internal permille format. - */ + if (ev->gas.index >= 0) + return ev->gas.index; + g = get_gasmix_from_event(ev); target_o2 = get_o2(g); target_he = get_he(g); @@ -318,13 +325,8 @@ int get_cylinder_index(struct dive *dive, struct event *ev) delta_o2 = get_o2(&cyl->gasmix) - target_o2; delta_he = get_he(&cyl->gasmix) - target_he; distance = delta_o2 * delta_o2; + distance += delta_he * delta_he; - /* Check the event type to figure out if we should care about the he part. - * SAMPLE_EVENT_GASCHANGE, aka without he - * SAMPLE_EVENT_GASCHANGE2, aka with he - */ - if (ev->type == SAMPLE_EVENT_GASCHANGE2) - distance += delta_he * delta_he; if (distance >= score) continue; score = distance; diff --git a/qt-ui/profile/diveeventitem.cpp b/qt-ui/profile/diveeventitem.cpp index b9bb4482b..c1ea48544 100644 --- a/qt-ui/profile/diveeventitem.cpp +++ b/qt-ui/profile/diveeventitem.cpp @@ -66,10 +66,10 @@ void DiveEventItem::setupPixmap() setPixmap(EVENT_PIXMAP(":flag")); } else if (strcmp(internalEvent->name, "heading") == 0) { setPixmap(EVENT_PIXMAP(":flag")); - } else if (internalEvent->type == SAMPLE_EVENT_GASCHANGE || internalEvent->type == SAMPLE_EVENT_GASCHANGE2) { - if (internalEvent->value >> 16) + } else if (event_is_gaschange(internalEvent)) { + if (internalEvent->gas.mix.he.permille) setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeTrimix")); - else if (internalEvent->value == 0) + else if (gasmix_is_air(&internalEvent->gas.mix)) setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeAir")); else setPixmap(EVENT_PIXMAP_BIGGER(":gaschangeNitrox")); @@ -86,7 +86,7 @@ void DiveEventItem::setupToolTipString() int value = internalEvent->value; int type = internalEvent->type; if (value) { - if (type == SAMPLE_EVENT_GASCHANGE || type == SAMPLE_EVENT_GASCHANGE2) { + if (event_is_gaschange(internalEvent)) { QModelIndexList result = dataModel->match(dataModel->index(0, DivePlotDataModel::TIME), Qt::DisplayRole, internalEvent->time.seconds); if (result.isEmpty()) { Q_ASSERT("can't find a spot in the dataModel"); diff --git a/save-git.c b/save-git.c index a1ef022fb..308a02c83 100644 --- a/save-git.c +++ b/save-git.c @@ -119,6 +119,18 @@ static void save_tags(struct membuffer *b, struct tag_entry *tags) put_string(b, "\n"); } +static void put_gasmix(struct membuffer *b, struct gasmix *mix) +{ + int o2 = mix->o2.permille; + int he = mix->he.permille; + + if (o2) { + put_format(b, " o2=%u.%u%%", FRACTION(o2, 10)); + if (he) + put_format(b, " he=%u.%u%%", FRACTION(he, 10)); + } +} + static void save_cylinder_info(struct membuffer *b, struct dive *dive) { int i, nr; @@ -128,8 +140,6 @@ static void save_cylinder_info(struct membuffer *b, struct dive *dive) cylinder_t *cylinder = dive->cylinder + i; int volume = cylinder->type.size.mliter; const char *description = cylinder->type.description; - int o2 = cylinder->gasmix.o2.permille; - int he = cylinder->gasmix.he.permille; put_string(b, "cylinder"); if (volume) @@ -137,11 +147,7 @@ static void save_cylinder_info(struct membuffer *b, struct dive *dive) put_pressure(b, cylinder->type.workingpressure, " workpressure=", "bar"); show_utf8(b, " description=", description, ""); strip_mb(b); - if (o2) { - put_format(b, " o2=%u.%u%%", FRACTION(o2, 10)); - if (he) - put_format(b, " he=%u.%u%%", FRACTION(he, 10)); - } + put_gasmix(b, &cylinder->gasmix); put_pressure(b, cylinder->start, " start=", "bar"); put_pressure(b, cylinder->end, " end=", "bar"); put_string(b, "\n"); @@ -292,6 +298,13 @@ static void save_one_event(struct membuffer *b, struct event *ev) show_index(b, ev->flags, "flags=", ""); show_index(b, ev->value, "value=", ""); show_utf8(b, " name=", ev->name, ""); + if (event_is_gaschange(ev)) { + if (ev->gas.index >= 0) { + show_index(b, ev->gas.index, "cylinder=", ""); + put_gasmix(b, &ev->gas.mix); + } else if (!event_gasmix_redundant(ev)) + put_gasmix(b, &ev->gas.mix); + } put_string(b, "\n"); } diff --git a/save-xml.c b/save-xml.c index fe804cb6d..e9f769d3f 100644 --- a/save-xml.c +++ b/save-xml.c @@ -148,6 +148,18 @@ static void save_overview(struct membuffer *b, struct dive *dive) show_utf8(b, dive->suit, " ", "\n", 0); } +static void put_gasmix(struct membuffer *b, struct gasmix *mix) +{ + int o2 = mix->o2.permille; + int he = mix->he.permille; + + if (o2) { + put_format(b, " o2='%u.%u%%'", FRACTION(o2, 10)); + if (he) + put_format(b, " he='%u.%u%%'", FRACTION(he, 10)); + } +} + static void save_cylinder_info(struct membuffer *b, struct dive *dive) { int i, nr; @@ -158,19 +170,13 @@ static void save_cylinder_info(struct membuffer *b, struct dive *dive) cylinder_t *cylinder = dive->cylinder + i; int volume = cylinder->type.size.mliter; const char *description = cylinder->type.description; - int o2 = cylinder->gasmix.o2.permille; - int he = cylinder->gasmix.he.permille; put_format(b, " type.workingpressure, " workpressure='", " bar'"); show_utf8(b, description, " description='", "'", 1); - if (o2) { - put_format(b, " o2='%u.%u%%'", FRACTION(o2, 10)); - if (he) - put_format(b, " he='%u.%u%%'", FRACTION(he, 10)); - } + put_gasmix(b, &cylinder->gasmix); put_pressure(b, cylinder->start, " start='", " bar'"); put_pressure(b, cylinder->end, " end='", " bar'"); put_format(b, " />\n"); @@ -261,6 +267,13 @@ static void save_one_event(struct membuffer *b, struct event *ev) show_index(b, ev->flags, "flags='", "'"); show_index(b, ev->value, "value='", "'"); show_utf8(b, ev->name, " name='", "'", 1); + if (event_is_gaschange(ev)) { + if (ev->gas.index >= 0) { + show_index(b, ev->gas.index, "cylinder='", "'"); + put_gasmix(b, &ev->gas.mix); + } else if (!event_gasmix_redundant(ev)) + put_gasmix(b, &ev->gas.mix); + } put_format(b, " />\n"); }