diff --git a/include/libdivecomputer/descriptor.h b/include/libdivecomputer/descriptor.h index 6eaba8ed..62b36738 100644 --- a/include/libdivecomputer/descriptor.h +++ b/include/libdivecomputer/descriptor.h @@ -124,6 +124,26 @@ dc_descriptor_get_transports (const dc_descriptor_t *descriptor); int dc_descriptor_filter (const dc_descriptor_t *descriptor, dc_transport_t transport, const void *userdata); +/** + * Find a descriptor by hardware identifier. + * + * Some device families share a coarse model number across multiple marketed + * products. When a device reports a hardware identifier (via + * DC_EVENT_DEVINFO.hw_id), this function can find a more specific + * descriptor than the model number alone provides. This is best-effort: + * returns NULL if the hardware id is unknown or not in the table. + * + * The returned descriptor is a direct reference to an internal table entry + * and must be released with dc_descriptor_free(), which is safe to call on + * such a reference. + * + * @param[in] family The device family type. + * @param[in] hw_id The hardware identifier from DC_EVENT_DEVINFO. + * @returns A descriptor on success, or NULL if no match. + */ +dc_descriptor_t * +dc_descriptor_find_by_hw_id (dc_family_t family, unsigned int hw_id); + #ifdef __cplusplus } #endif /* __cplusplus */ diff --git a/include/libdivecomputer/device.h b/include/libdivecomputer/device.h index 8810a652..9ffce79e 100644 --- a/include/libdivecomputer/device.h +++ b/include/libdivecomputer/device.h @@ -52,6 +52,13 @@ typedef struct dc_event_devinfo_t { unsigned int model; unsigned int firmware; unsigned int serial; + /* Generic hardware identifier, 0 = unknown/not available. + * On live download from devices that support it, this is populated + * with the device's hardware type identifier (e.g. Shearwater FWID + * from RDBI 0x8050). It supplements the coarse model field for + * consumer-side sub-model differentiation. Consumers that do not use + * this field are unaffected; the model field remains authoritative. */ + unsigned int hw_id; } dc_event_devinfo_t; typedef struct dc_event_clock_t { diff --git a/src/cochran_commander.c b/src/cochran_commander.c index 83f88009..44552150 100644 --- a/src/cochran_commander.c +++ b/src/cochran_commander.c @@ -937,7 +937,6 @@ cochran_commander_device_foreach (dc_device_t *abstract, dc_dive_callback_t call devinfo.serial = array_uint32_word_be(data.config + layout->cf_serial_number); else devinfo.serial = array_uint32_le(data.config + layout->cf_serial_number); - device_event_emit (abstract, DC_EVENT_DEVINFO, &devinfo); unsigned int head_dive = 0, tail_dive = 0, dive_count = 0; diff --git a/src/descriptor.c b/src/descriptor.c index 8266fc85..8608fc98 100644 --- a/src/descriptor.c +++ b/src/descriptor.c @@ -97,6 +97,38 @@ static const dc_iterator_vtable_t dc_descriptor_iterator_vtable = { * actually used to identify individual models, identical values are assigned. */ +/* Hardware-identifier to descriptor mapping. + * + * Some device families share a coarse model number (DC_EVENT_DEVINFO.model) + * across multiple marketed products; the hardware identifier (hw_id) + * from DC_EVENT_DEVINFO can refine the selection to a specific descriptor. + * This table is best-effort: the FWID is only available on live download and + * may change across firmware updates. Consumers should treat a NULL result + * from dc_descriptor_find_by_hw_id() as "use the coarse model descriptor". */ +typedef struct { + dc_family_t family; + unsigned int model; + unsigned int hw_id; + const char *product; +} dc_hw_id_entry_t; + +static const dc_hw_id_entry_t g_hw_id_map[] = { + /* Shearwater Petrel family. + * Petrel 1 (model=3): hardware ids 0x0404, 0x0909 */ + {DC_FAMILY_SHEARWATER_PETREL, 3, 0x0404, "Petrel"}, + {DC_FAMILY_SHEARWATER_PETREL, 3, 0x0909, "Petrel"}, + /* Petrel 2 (model=3): hardware ids 0x0505, 0x0808, 0x0838, 0x08A5, + * 0x0B0B, 0x7828, 0x7B2C, 0x8838 */ + {DC_FAMILY_SHEARWATER_PETREL, 3, 0x0505, "Petrel 2"}, + {DC_FAMILY_SHEARWATER_PETREL, 3, 0x0808, "Petrel 2"}, + {DC_FAMILY_SHEARWATER_PETREL, 3, 0x0838, "Petrel 2"}, + {DC_FAMILY_SHEARWATER_PETREL, 3, 0x08A5, "Petrel 2"}, + {DC_FAMILY_SHEARWATER_PETREL, 3, 0x0B0B, "Petrel 2"}, + {DC_FAMILY_SHEARWATER_PETREL, 3, 0x7828, "Petrel 2"}, + {DC_FAMILY_SHEARWATER_PETREL, 3, 0x7B2C, "Petrel 2"}, + {DC_FAMILY_SHEARWATER_PETREL, 3, 0x8838, "Petrel 2"}, +}; + static const dc_descriptor_t g_descriptors[] = { /* Suunto Solution */ {"Suunto", "Solution", DC_FAMILY_SUUNTO_SOLUTION, 0, DC_TRANSPORT_SERIAL, NULL}, @@ -1055,6 +1087,36 @@ dc_descriptor_free (dc_descriptor_t *descriptor) return; } +dc_descriptor_t * +dc_descriptor_find_by_hw_id (dc_family_t family, unsigned int hw_id) +{ + if (hw_id == 0) + return NULL; + + for (size_t i = 0; i < C_ARRAY_SIZE(g_hw_id_map); i++) { + if (g_hw_id_map[i].family != family || g_hw_id_map[i].hw_id != hw_id) + continue; + + /* Matched — find the corresponding entry in g_descriptors[] by + * family, model, and product name. Returning a direct pointer + * into the static table is safe: dc_descriptor_free() is a + * no-op and g_descriptors[] has static storage duration. */ + for (size_t j = 0; j < C_ARRAY_SIZE(g_descriptors); j++) { + if (g_descriptors[j].type == family && + g_descriptors[j].model == g_hw_id_map[i].model && + strcmp(g_descriptors[j].product, g_hw_id_map[i].product) == 0) { + return (dc_descriptor_t *) &g_descriptors[j]; + } + } + /* hw_id matched the map but the descriptor was not found — + * product name mismatch or g_descriptors[] was edited without + * updating g_hw_id_map[]. */ + return NULL; + } + + return NULL; +} + const char * dc_descriptor_get_vendor (const dc_descriptor_t *descriptor) { diff --git a/src/device-private.h b/src/device-private.h index 04ee68b8..016ea1c7 100644 --- a/src/device-private.h +++ b/src/device-private.h @@ -88,6 +88,9 @@ dc_device_deallocate (dc_device_t *device); void device_event_emit (dc_device_t *device, dc_event_type_t event, const void *data); +void +device_set_hw_id (dc_device_t *device, unsigned int hw_id); + int device_is_cancelled (dc_device_t *device); diff --git a/src/device.c b/src/device.c index 4950b42c..e09b0eae 100644 --- a/src/device.c +++ b/src/device.c @@ -462,6 +462,13 @@ dc_device_close (dc_device_t *device) } +// AI-generated (Claude) +void +device_set_hw_id (dc_device_t *device, unsigned int hw_id) +{ + device->devinfo.hw_id = hw_id; +} + void device_event_emit (dc_device_t *device, dc_event_type_t event, const void *data) { @@ -493,7 +500,13 @@ device_event_emit (dc_device_t *device, dc_event_type_t event, const void *data) // Cache the event data. switch (event) { case DC_EVENT_DEVINFO: - device->devinfo = *(const dc_event_devinfo_t *) data; + { + const dc_event_devinfo_t *devinfo = (const dc_event_devinfo_t *) data; + device->devinfo.model = devinfo->model; + device->devinfo.firmware = devinfo->firmware; + device->devinfo.serial = devinfo->serial; + data = &device->devinfo; + } break; case DC_EVENT_CLOCK: device->clock = *(const dc_event_clock_t *) data; diff --git a/src/libdivecomputer.symbols b/src/libdivecomputer.symbols index 48bdabef..d1b78917 100644 --- a/src/libdivecomputer.symbols +++ b/src/libdivecomputer.symbols @@ -36,6 +36,7 @@ dc_descriptor_get_type dc_descriptor_get_model dc_descriptor_get_transports dc_descriptor_filter +dc_descriptor_find_by_hw_id dc_iostream_get_transport dc_iostream_set_timeout diff --git a/src/shearwater_common.c b/src/shearwater_common.c index 22ae3ca6..0e838f73 100644 --- a/src/shearwater_common.c +++ b/src/shearwater_common.c @@ -762,110 +762,3 @@ shearwater_common_timesync_utc (shearwater_common_device_t *device, const dc_dat return status; } - -dc_status_t shearwater_common_get_model(shearwater_common_device_t *device, unsigned int *model) -{ - // Read the hardware type. - unsigned char rsp_hardware[2] = {0}; - dc_status_t status = shearwater_common_rdbi (device, ID_HARDWARE, rsp_hardware, sizeof(rsp_hardware), NULL); - if (status != DC_STATUS_SUCCESS) { - ERROR (device->base.context, "Failed to read the hardware type."); - return status; - } - - // Convert and map to the model number. - unsigned int hardware = array_uint16_be (rsp_hardware); - - DEBUG(device->base.context, "Hardware type: 0x%04x", hardware); - - switch (hardware) { - case 0x0101: - case 0x0202: - *model = PREDATOR; - break; - case 0x0404: - case 0x0909: - *model = PETREL; - break; - case 0x0505: - case 0x0808: - case 0x0838: - case 0x08A5: - case 0x0B0B: - case 0x7828: - case 0x7B2C: - case 0x8838: - *model = PETREL2; - break; - case 0xB407: - case 0xB429: - case 0xB469: - case 0x3C3D: - *model = PETREL3; - break; - case 0x0606: - case 0x0A0A: - *model = NERD; - break; - case 0x0E0D: - case 0x7E2D: - *model = NERD2; - break; - case 0x0707: - *model = PERDIX; - break; - case 0x0C0D: - case 0x425B: - case 0x7C2D: - case 0x8D6C: - *model = PERDIXAI; - break; - case 0x704C: - case 0x924C: - case 0x9C64: - case 0xC407: - case 0xC429: - case 0xC964: - *model = PERDIX2; - break; - case 0x39C2: - case 0x4AB1: - *model = PERDIX3; - break; - case 0x1F0A: - case 0x1F0F: - case 0x0F0F: - case 0x1F10: - case 0x1F1A: - *model = TERIC; - break; - case 0x1512: - case 0x1613: - case 0x2623: - case 0x63A5: - *model = PEREGRINE; - break; - case 0x1712: - case 0x813A: - *model = PEREGRINE_TX; - break; - case 0xC0E0: - *model = TERN; - break; - default: - // Unknown hardware type: fall back to reading the model number directly from the device. - WARNING (device->base.context, "Unknown hardware type 0x%04x, falling back to ID_MODEL.", hardware); - { - unsigned char rsp_model = 0; - dc_status_t rc = shearwater_common_rdbi (device, ID_MODEL, &rsp_model, sizeof(rsp_model), NULL); - if (rc != DC_STATUS_SUCCESS) { - ERROR (device->base.context, "Failed to read the model number."); - return rc; - } - *model = rsp_model; - } - break; - } - - return status; -} diff --git a/src/shearwater_common.h b/src/shearwater_common.h index 9808875b..cb870e09 100644 --- a/src/shearwater_common.h +++ b/src/shearwater_common.h @@ -46,6 +46,9 @@ extern "C" { #define PREDATOR 2 #define PETREL 3 +/* PETREL2: Petrel 2 hardware reports ID_MODEL=PETREL (3); there is no + * distinct Product-Version value for it. FWID-based sub-model distinction + * is exposed via devinfo.hw_id on live download. */ #define PETREL2 PETREL #define NERD 4 #define PERDIX 5 @@ -93,8 +96,6 @@ dc_status_t shearwater_common_can_wdbi (shearwater_common_device_t *device, dc_b dc_status_t shearwater_common_device_timesync(dc_device_t *abstract, const dc_datetime_t *datetime); -dc_status_t shearwater_common_get_model(shearwater_common_device_t *device, unsigned int *model); - #ifdef __cplusplus } #endif /* __cplusplus */ diff --git a/src/shearwater_petrel.c b/src/shearwater_petrel.c index 3a9af132..bfbb68f3 100644 --- a/src/shearwater_petrel.c +++ b/src/shearwater_petrel.c @@ -190,16 +190,33 @@ shearwater_petrel_device_foreach (dc_device_t *abstract, dc_dive_callback_t call // Convert to a number. unsigned int firmware = str2num (rsp_firmware, rsp_firmware_length, 1); - unsigned int model = 0; - rc = shearwater_common_get_model (&device->base, &model); - if (rc != DC_STATUS_SUCCESS) + // Read the model number (ID_MODEL, RDBI 0x8060) — Product-Version byte, + // authoritative for the coarse product family (aligns with upstream libdc). + unsigned char rsp_model = 0; + rc = shearwater_common_rdbi (&device->base, ID_MODEL, &rsp_model, sizeof(rsp_model), NULL); + if (rc != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to read the model number."); return rc; + } + + // Attempt to read the hardware type (ID_HARDWARE, RDBI 0x8050) for + // consumer-side sub-model differentiation (e.g. Petrel 1 vs Petrel 2). + // Best-effort: FWID can change across firmware updates and stored logs + // do not carry it. Failure is non-fatal; hw_id remains 0. + unsigned char rsp_hardware[2] = {0}; + unsigned int fwid = 0; + dc_status_t hw_rc = shearwater_common_rdbi (&device->base, ID_HARDWARE, rsp_hardware, sizeof(rsp_hardware), NULL); + if (hw_rc == DC_STATUS_SUCCESS) { + fwid = array_uint16_be (rsp_hardware); + DEBUG (abstract->context, "Hardware type (FWID): 0x%04x", fwid); + } // Emit a device info event. dc_event_devinfo_t devinfo; - devinfo.model = model; + devinfo.model = rsp_model; devinfo.firmware = firmware; devinfo.serial = array_uint32_be (serial); + device_set_hw_id (abstract, fwid); device_event_emit (abstract, DC_EVENT_DEVINFO, &devinfo); // Read the logbook type @@ -356,15 +373,17 @@ shearwater_petrel_device_foreach (dc_device_t *abstract, dc_dive_callback_t call static dc_status_t shearwater_petrel_device_timesync (dc_device_t *abstract, const dc_datetime_t *datetime) { - dc_status_t status = DC_STATUS_SUCCESS; shearwater_common_device_t *device = (shearwater_common_device_t *) abstract; - unsigned int model = 0; - status = shearwater_common_get_model (device, &model); - if (status != DC_STATUS_SUCCESS) + // Read ID_MODEL (RDBI 0x8060) directly to determine time-sync variant. + unsigned char rsp_model = 0; + dc_status_t status = shearwater_common_rdbi (device, ID_MODEL, &rsp_model, sizeof(rsp_model), NULL); + if (status != DC_STATUS_SUCCESS) { + ERROR (abstract->context, "Failed to read the model number."); return status; + } - if (model == TERIC) { + if (rsp_model == TERIC) { return shearwater_common_timesync_utc (device, datetime); } else { return shearwater_common_timesync_local (device, datetime); diff --git a/src/shearwater_predator_parser.c b/src/shearwater_predator_parser.c index e07bc8ed..ba730d4d 100644 --- a/src/shearwater_predator_parser.c +++ b/src/shearwater_predator_parser.c @@ -873,6 +873,11 @@ shearwater_predator_parser_cache (shearwater_predator_parser_t *parser) } // Get the correct model number from the final block. + // Product-Version byte from the device's final record — authoritative for + // the coarse product family. This value is not the FWID; stored logs do not + // carry the FWID, so sub-model distinctions (e.g. Petrel 1 vs Petrel 2) are + // not recoverable on re-parse. Consumer-side refinement uses the FWID + // exposed via devinfo.hw_id on live download. if (parser->final != UNDEFINED) { parser->model = data[parser->final + 13]; DEBUG (abstract->context, "Device: model=%u, serial=%u, firmware=%u",