1 | // SPDX-License-Identifier: GPL-2.0 |
2 | |
3 | /* Driver for ETAS GmbH ES58X USB CAN(-FD) Bus Interfaces. |
4 | * |
5 | * File es58x_devlink.c: report the product information using devlink. |
6 | * |
7 | * Copyright (c) 2022 Vincent Mailhol <mailhol.vincent@wanadoo.fr> |
8 | */ |
9 | |
10 | #include <linux/ctype.h> |
11 | #include <linux/device.h> |
12 | #include <linux/usb.h> |
13 | #include <net/devlink.h> |
14 | |
15 | #include "es58x_core.h" |
16 | |
17 | /* USB descriptor index containing the product information string. */ |
18 | #define ES58X_PROD_INFO_IDX 6 |
19 | |
20 | /** |
21 | * es58x_parse_sw_version() - Extract boot loader or firmware version. |
22 | * @es58x_dev: ES58X device. |
23 | * @prod_info: USB custom string returned by the device. |
24 | * @prefix: Select which information should be parsed. Set it to "FW" |
25 | * to parse the firmware version or to "BL" to parse the |
26 | * bootloader version. |
27 | * |
28 | * The @prod_info string contains the firmware and the bootloader |
29 | * version number all prefixed by a magic string and concatenated with |
30 | * other numbers. Depending on the device, the firmware (bootloader) |
31 | * format is either "FW_Vxx.xx.xx" ("BL_Vxx.xx.xx") or "FW:xx.xx.xx" |
32 | * ("BL:xx.xx.xx") where 'x' represents a digit. @prod_info must |
33 | * contains the common part of those prefixes: "FW" or "BL". |
34 | * |
35 | * Parse @prod_info and store the version number in |
36 | * &es58x_dev.firmware_version or &es58x_dev.bootloader_version |
37 | * according to @prefix value. |
38 | * |
39 | * Return: zero on success, -EINVAL if @prefix contains an invalid |
40 | * value and -EBADMSG if @prod_info could not be parsed. |
41 | */ |
42 | static int es58x_parse_sw_version(struct es58x_device *es58x_dev, |
43 | const char *prod_info, const char *prefix) |
44 | { |
45 | struct es58x_sw_version *version; |
46 | int major, minor, revision; |
47 | |
48 | if (!strcmp(prefix, "FW" )) |
49 | version = &es58x_dev->firmware_version; |
50 | else if (!strcmp(prefix, "BL" )) |
51 | version = &es58x_dev->bootloader_version; |
52 | else |
53 | return -EINVAL; |
54 | |
55 | /* Go to prefix */ |
56 | prod_info = strstr(prod_info, prefix); |
57 | if (!prod_info) |
58 | return -EBADMSG; |
59 | /* Go to beginning of the version number */ |
60 | while (!isdigit(c: *prod_info)) { |
61 | prod_info++; |
62 | if (!*prod_info) |
63 | return -EBADMSG; |
64 | } |
65 | |
66 | if (sscanf(prod_info, "%2u.%2u.%2u" , &major, &minor, &revision) != 3) |
67 | return -EBADMSG; |
68 | |
69 | version->major = major; |
70 | version->minor = minor; |
71 | version->revision = revision; |
72 | |
73 | return 0; |
74 | } |
75 | |
76 | /** |
77 | * es58x_parse_hw_rev() - Extract hardware revision number. |
78 | * @es58x_dev: ES58X device. |
79 | * @prod_info: USB custom string returned by the device. |
80 | * |
81 | * @prod_info contains the hardware revision prefixed by a magic |
82 | * string and conquenated together with other numbers. Depending on |
83 | * the device, the hardware revision format is either |
84 | * "HW_VER:axxx/xxx" or "HR:axxx/xxx" where 'a' represents a letter |
85 | * and 'x' a digit. |
86 | * |
87 | * Parse @prod_info and store the hardware revision number in |
88 | * &es58x_dev.hardware_revision. |
89 | * |
90 | * Return: zero on success, -EBADMSG if @prod_info could not be |
91 | * parsed. |
92 | */ |
93 | static int es58x_parse_hw_rev(struct es58x_device *es58x_dev, |
94 | const char *prod_info) |
95 | { |
96 | char letter; |
97 | int major, minor; |
98 | |
99 | /* The only occurrence of 'H' is in the hardware revision prefix. */ |
100 | prod_info = strchr(prod_info, 'H'); |
101 | if (!prod_info) |
102 | return -EBADMSG; |
103 | /* Go to beginning of the hardware revision */ |
104 | prod_info = strchr(prod_info, ':'); |
105 | if (!prod_info) |
106 | return -EBADMSG; |
107 | prod_info++; |
108 | |
109 | if (sscanf(prod_info, "%c%3u/%3u" , &letter, &major, &minor) != 3) |
110 | return -EBADMSG; |
111 | |
112 | es58x_dev->hardware_revision.letter = letter; |
113 | es58x_dev->hardware_revision.major = major; |
114 | es58x_dev->hardware_revision.minor = minor; |
115 | |
116 | return 0; |
117 | } |
118 | |
119 | /** |
120 | * es58x_parse_product_info() - Parse the ES58x product information |
121 | * string. |
122 | * @es58x_dev: ES58X device. |
123 | * |
124 | * Retrieve the product information string and parse it to extract the |
125 | * firmware version, the bootloader version and the hardware |
126 | * revision. |
127 | * |
128 | * If the function fails, set the version or revision to an invalid |
129 | * value and emit an informal message. Continue probing because the |
130 | * product information is not critical for the driver to operate. |
131 | */ |
132 | void es58x_parse_product_info(struct es58x_device *es58x_dev) |
133 | { |
134 | static const struct es58x_sw_version sw_version_not_set = { |
135 | .major = -1, |
136 | .minor = -1, |
137 | .revision = -1, |
138 | }; |
139 | static const struct es58x_hw_revision hw_revision_not_set = { |
140 | .letter = '\0', |
141 | .major = -1, |
142 | .minor = -1, |
143 | }; |
144 | char *prod_info; |
145 | |
146 | es58x_dev->firmware_version = sw_version_not_set; |
147 | es58x_dev->bootloader_version = sw_version_not_set; |
148 | es58x_dev->hardware_revision = hw_revision_not_set; |
149 | |
150 | prod_info = usb_cache_string(udev: es58x_dev->udev, ES58X_PROD_INFO_IDX); |
151 | if (!prod_info) { |
152 | dev_warn(es58x_dev->dev, |
153 | "could not retrieve the product info string\n" ); |
154 | return; |
155 | } |
156 | |
157 | if (es58x_parse_sw_version(es58x_dev, prod_info, prefix: "FW" ) || |
158 | es58x_parse_sw_version(es58x_dev, prod_info, prefix: "BL" ) || |
159 | es58x_parse_hw_rev(es58x_dev, prod_info)) |
160 | dev_info(es58x_dev->dev, |
161 | "could not parse product info: '%s'\n" , prod_info); |
162 | |
163 | kfree(objp: prod_info); |
164 | } |
165 | |
166 | /** |
167 | * es58x_sw_version_is_valid() - Check if the version is a valid number. |
168 | * @sw_ver: Version number of either the firmware or the bootloader. |
169 | * |
170 | * If any of the software version sub-numbers do not fit on two |
171 | * digits, the version is invalid, most probably because the product |
172 | * string could not be parsed. |
173 | * |
174 | * Return: @true if the software version is valid, @false otherwise. |
175 | */ |
176 | static inline bool es58x_sw_version_is_valid(struct es58x_sw_version *sw_ver) |
177 | { |
178 | return sw_ver->major < 100 && sw_ver->minor < 100 && |
179 | sw_ver->revision < 100; |
180 | } |
181 | |
182 | /** |
183 | * es58x_hw_revision_is_valid() - Check if the revision is a valid number. |
184 | * @hw_rev: Revision number of the hardware. |
185 | * |
186 | * If &es58x_hw_revision.letter is not a alphanumeric character or if |
187 | * any of the hardware revision sub-numbers do not fit on three |
188 | * digits, the revision is invalid, most probably because the product |
189 | * string could not be parsed. |
190 | * |
191 | * Return: @true if the hardware revision is valid, @false otherwise. |
192 | */ |
193 | static inline bool es58x_hw_revision_is_valid(struct es58x_hw_revision *hw_rev) |
194 | { |
195 | return isalnum(hw_rev->letter) && hw_rev->major < 1000 && |
196 | hw_rev->minor < 1000; |
197 | } |
198 | |
199 | /** |
200 | * es58x_devlink_info_get() - Report the product information. |
201 | * @devlink: Devlink. |
202 | * @req: skb wrapper where to put requested information. |
203 | * @extack: Unused. |
204 | * |
205 | * Report the firmware version, the bootloader version, the hardware |
206 | * revision and the serial number through netlink. |
207 | * |
208 | * Return: zero on success, errno when any error occurs. |
209 | */ |
210 | static int es58x_devlink_info_get(struct devlink *devlink, |
211 | struct devlink_info_req *req, |
212 | struct netlink_ext_ack *extack) |
213 | { |
214 | struct es58x_device *es58x_dev = devlink_priv(devlink); |
215 | struct es58x_sw_version *fw_ver = &es58x_dev->firmware_version; |
216 | struct es58x_sw_version *bl_ver = &es58x_dev->bootloader_version; |
217 | struct es58x_hw_revision *hw_rev = &es58x_dev->hardware_revision; |
218 | char buf[max(sizeof("xx.xx.xx" ), sizeof("axxx/xxx" ))]; |
219 | int ret = 0; |
220 | |
221 | if (es58x_sw_version_is_valid(sw_ver: fw_ver)) { |
222 | snprintf(buf, size: sizeof(buf), fmt: "%02u.%02u.%02u" , |
223 | fw_ver->major, fw_ver->minor, fw_ver->revision); |
224 | ret = devlink_info_version_running_put(req, |
225 | DEVLINK_INFO_VERSION_GENERIC_FW, |
226 | version_value: buf); |
227 | if (ret) |
228 | return ret; |
229 | } |
230 | |
231 | if (es58x_sw_version_is_valid(sw_ver: bl_ver)) { |
232 | snprintf(buf, size: sizeof(buf), fmt: "%02u.%02u.%02u" , |
233 | bl_ver->major, bl_ver->minor, bl_ver->revision); |
234 | ret = devlink_info_version_running_put(req, |
235 | DEVLINK_INFO_VERSION_GENERIC_FW_BOOTLOADER, |
236 | version_value: buf); |
237 | if (ret) |
238 | return ret; |
239 | } |
240 | |
241 | if (es58x_hw_revision_is_valid(hw_rev)) { |
242 | snprintf(buf, size: sizeof(buf), fmt: "%c%03u/%03u" , |
243 | hw_rev->letter, hw_rev->major, hw_rev->minor); |
244 | ret = devlink_info_version_fixed_put(req, |
245 | DEVLINK_INFO_VERSION_GENERIC_BOARD_REV, |
246 | version_value: buf); |
247 | if (ret) |
248 | return ret; |
249 | } |
250 | |
251 | return devlink_info_serial_number_put(req, sn: es58x_dev->udev->serial); |
252 | } |
253 | |
254 | const struct devlink_ops es58x_dl_ops = { |
255 | .info_get = es58x_devlink_info_get, |
256 | }; |
257 | |