1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * zopt2201.c - Support for IDT ZOPT2201 ambient light and UV B sensor |
4 | * |
5 | * Copyright 2017 Peter Meerwald-Stadler <pmeerw@pmeerw.net> |
6 | * |
7 | * Datasheet: https://www.idt.com/document/dst/zopt2201-datasheet |
8 | * 7-bit I2C slave addresses 0x53 (default) or 0x52 (programmed) |
9 | * |
10 | * TODO: interrupt support, ALS/UVB raw mode |
11 | */ |
12 | |
13 | #include <linux/module.h> |
14 | #include <linux/i2c.h> |
15 | #include <linux/mutex.h> |
16 | #include <linux/err.h> |
17 | #include <linux/delay.h> |
18 | |
19 | #include <linux/iio/iio.h> |
20 | #include <linux/iio/sysfs.h> |
21 | |
22 | #include <asm/unaligned.h> |
23 | |
24 | #define ZOPT2201_DRV_NAME "zopt2201" |
25 | |
26 | /* Registers */ |
27 | #define ZOPT2201_MAIN_CTRL 0x00 |
28 | #define ZOPT2201_LS_MEAS_RATE 0x04 |
29 | #define ZOPT2201_LS_GAIN 0x05 |
30 | #define ZOPT2201_PART_ID 0x06 |
31 | #define ZOPT2201_MAIN_STATUS 0x07 |
32 | #define ZOPT2201_ALS_DATA 0x0d /* LSB first, 13 to 20 bits */ |
33 | #define ZOPT2201_UVB_DATA 0x10 /* LSB first, 13 to 20 bits */ |
34 | #define ZOPT2201_UV_COMP_DATA 0x13 /* LSB first, 13 to 20 bits */ |
35 | #define ZOPT2201_COMP_DATA 0x16 /* LSB first, 13 to 20 bits */ |
36 | #define ZOPT2201_INT_CFG 0x19 |
37 | #define ZOPT2201_INT_PST 0x1a |
38 | |
39 | #define ZOPT2201_MAIN_CTRL_LS_MODE BIT(3) /* 0 .. ALS, 1 .. UV B */ |
40 | #define ZOPT2201_MAIN_CTRL_LS_EN BIT(1) |
41 | |
42 | /* Values for ZOPT2201_LS_MEAS_RATE resolution / bit width */ |
43 | #define ZOPT2201_MEAS_RES_20BIT 0 /* takes 400 ms */ |
44 | #define ZOPT2201_MEAS_RES_19BIT 1 /* takes 200 ms */ |
45 | #define ZOPT2201_MEAS_RES_18BIT 2 /* takes 100 ms, default */ |
46 | #define ZOPT2201_MEAS_RES_17BIT 3 /* takes 50 ms */ |
47 | #define ZOPT2201_MEAS_RES_16BIT 4 /* takes 25 ms */ |
48 | #define ZOPT2201_MEAS_RES_13BIT 5 /* takes 3.125 ms */ |
49 | #define ZOPT2201_MEAS_RES_SHIFT 4 |
50 | |
51 | /* Values for ZOPT2201_LS_MEAS_RATE measurement rate */ |
52 | #define ZOPT2201_MEAS_FREQ_25MS 0 |
53 | #define ZOPT2201_MEAS_FREQ_50MS 1 |
54 | #define ZOPT2201_MEAS_FREQ_100MS 2 /* default */ |
55 | #define ZOPT2201_MEAS_FREQ_200MS 3 |
56 | #define ZOPT2201_MEAS_FREQ_500MS 4 |
57 | #define ZOPT2201_MEAS_FREQ_1000MS 5 |
58 | #define ZOPT2201_MEAS_FREQ_2000MS 6 |
59 | |
60 | /* Values for ZOPT2201_LS_GAIN */ |
61 | #define ZOPT2201_LS_GAIN_1 0 |
62 | #define ZOPT2201_LS_GAIN_3 1 |
63 | #define ZOPT2201_LS_GAIN_6 2 |
64 | #define ZOPT2201_LS_GAIN_9 3 |
65 | #define ZOPT2201_LS_GAIN_18 4 |
66 | |
67 | /* Values for ZOPT2201_MAIN_STATUS */ |
68 | #define ZOPT2201_MAIN_STATUS_POWERON BIT(5) |
69 | #define ZOPT2201_MAIN_STATUS_INT BIT(4) |
70 | #define ZOPT2201_MAIN_STATUS_DRDY BIT(3) |
71 | |
72 | #define ZOPT2201_PART_NUMBER 0xb2 |
73 | |
74 | struct zopt2201_data { |
75 | struct i2c_client *client; |
76 | struct mutex lock; |
77 | u8 gain; |
78 | u8 res; |
79 | u8 rate; |
80 | }; |
81 | |
82 | static const struct { |
83 | unsigned int gain; /* gain factor */ |
84 | unsigned int scale; /* micro lux per count */ |
85 | } zopt2201_gain_als[] = { |
86 | { 1, 19200000 }, |
87 | { 3, 6400000 }, |
88 | { 6, 3200000 }, |
89 | { 9, 2133333 }, |
90 | { 18, 1066666 }, |
91 | }; |
92 | |
93 | static const struct { |
94 | unsigned int gain; /* gain factor */ |
95 | unsigned int scale; /* micro W/m2 per count */ |
96 | } zopt2201_gain_uvb[] = { |
97 | { 1, 460800 }, |
98 | { 3, 153600 }, |
99 | { 6, 76800 }, |
100 | { 9, 51200 }, |
101 | { 18, 25600 }, |
102 | }; |
103 | |
104 | static const struct { |
105 | unsigned int bits; /* sensor resolution in bits */ |
106 | unsigned long us; /* measurement time in micro seconds */ |
107 | } zopt2201_resolution[] = { |
108 | { 20, 400000 }, |
109 | { 19, 200000 }, |
110 | { 18, 100000 }, |
111 | { 17, 50000 }, |
112 | { 16, 25000 }, |
113 | { 13, 3125 }, |
114 | }; |
115 | |
116 | static const struct { |
117 | unsigned int scale, uscale; /* scale factor as integer + micro */ |
118 | u8 gain; /* gain register value */ |
119 | u8 res; /* resolution register value */ |
120 | } zopt2201_scale_als[] = { |
121 | { 19, 200000, 0, 5 }, |
122 | { 6, 400000, 1, 5 }, |
123 | { 3, 200000, 2, 5 }, |
124 | { 2, 400000, 0, 4 }, |
125 | { 2, 133333, 3, 5 }, |
126 | { 1, 200000, 0, 3 }, |
127 | { 1, 66666, 4, 5 }, |
128 | { 0, 800000, 1, 4 }, |
129 | { 0, 600000, 0, 2 }, |
130 | { 0, 400000, 2, 4 }, |
131 | { 0, 300000, 0, 1 }, |
132 | { 0, 266666, 3, 4 }, |
133 | { 0, 200000, 2, 3 }, |
134 | { 0, 150000, 0, 0 }, |
135 | { 0, 133333, 4, 4 }, |
136 | { 0, 100000, 2, 2 }, |
137 | { 0, 66666, 4, 3 }, |
138 | { 0, 50000, 2, 1 }, |
139 | { 0, 33333, 4, 2 }, |
140 | { 0, 25000, 2, 0 }, |
141 | { 0, 16666, 4, 1 }, |
142 | { 0, 8333, 4, 0 }, |
143 | }; |
144 | |
145 | static const struct { |
146 | unsigned int scale, uscale; /* scale factor as integer + micro */ |
147 | u8 gain; /* gain register value */ |
148 | u8 res; /* resolution register value */ |
149 | } zopt2201_scale_uvb[] = { |
150 | { 0, 460800, 0, 5 }, |
151 | { 0, 153600, 1, 5 }, |
152 | { 0, 76800, 2, 5 }, |
153 | { 0, 57600, 0, 4 }, |
154 | { 0, 51200, 3, 5 }, |
155 | { 0, 28800, 0, 3 }, |
156 | { 0, 25600, 4, 5 }, |
157 | { 0, 19200, 1, 4 }, |
158 | { 0, 14400, 0, 2 }, |
159 | { 0, 9600, 2, 4 }, |
160 | { 0, 7200, 0, 1 }, |
161 | { 0, 6400, 3, 4 }, |
162 | { 0, 4800, 2, 3 }, |
163 | { 0, 3600, 0, 0 }, |
164 | { 0, 3200, 4, 4 }, |
165 | { 0, 2400, 2, 2 }, |
166 | { 0, 1600, 4, 3 }, |
167 | { 0, 1200, 2, 1 }, |
168 | { 0, 800, 4, 2 }, |
169 | { 0, 600, 2, 0 }, |
170 | { 0, 400, 4, 1 }, |
171 | { 0, 200, 4, 0 }, |
172 | }; |
173 | |
174 | static int zopt2201_enable_mode(struct zopt2201_data *data, bool uvb_mode) |
175 | { |
176 | u8 out = ZOPT2201_MAIN_CTRL_LS_EN; |
177 | |
178 | if (uvb_mode) |
179 | out |= ZOPT2201_MAIN_CTRL_LS_MODE; |
180 | |
181 | return i2c_smbus_write_byte_data(client: data->client, ZOPT2201_MAIN_CTRL, value: out); |
182 | } |
183 | |
184 | static int zopt2201_read(struct zopt2201_data *data, u8 reg) |
185 | { |
186 | struct i2c_client *client = data->client; |
187 | int tries = 10; |
188 | u8 buf[3]; |
189 | int ret; |
190 | |
191 | mutex_lock(&data->lock); |
192 | ret = zopt2201_enable_mode(data, uvb_mode: reg == ZOPT2201_UVB_DATA); |
193 | if (ret < 0) |
194 | goto fail; |
195 | |
196 | while (tries--) { |
197 | unsigned long t = zopt2201_resolution[data->res].us; |
198 | |
199 | if (t <= 20000) |
200 | usleep_range(min: t, max: t + 1000); |
201 | else |
202 | msleep(msecs: t / 1000); |
203 | ret = i2c_smbus_read_byte_data(client, ZOPT2201_MAIN_STATUS); |
204 | if (ret < 0) |
205 | goto fail; |
206 | if (ret & ZOPT2201_MAIN_STATUS_DRDY) |
207 | break; |
208 | } |
209 | |
210 | if (tries < 0) { |
211 | ret = -ETIMEDOUT; |
212 | goto fail; |
213 | } |
214 | |
215 | ret = i2c_smbus_read_i2c_block_data(client, command: reg, length: sizeof(buf), values: buf); |
216 | if (ret < 0) |
217 | goto fail; |
218 | |
219 | ret = i2c_smbus_write_byte_data(client, ZOPT2201_MAIN_CTRL, value: 0x00); |
220 | if (ret < 0) |
221 | goto fail; |
222 | mutex_unlock(lock: &data->lock); |
223 | |
224 | return get_unaligned_le24(p: &buf[0]); |
225 | |
226 | fail: |
227 | mutex_unlock(lock: &data->lock); |
228 | return ret; |
229 | } |
230 | |
231 | static const struct iio_chan_spec zopt2201_channels[] = { |
232 | { |
233 | .type = IIO_LIGHT, |
234 | .address = ZOPT2201_ALS_DATA, |
235 | .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | |
236 | BIT(IIO_CHAN_INFO_SCALE), |
237 | .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_INT_TIME), |
238 | }, |
239 | { |
240 | .type = IIO_INTENSITY, |
241 | .modified = 1, |
242 | .channel2 = IIO_MOD_LIGHT_UV, |
243 | .address = ZOPT2201_UVB_DATA, |
244 | .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | |
245 | BIT(IIO_CHAN_INFO_SCALE), |
246 | .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_INT_TIME), |
247 | }, |
248 | { |
249 | .type = IIO_UVINDEX, |
250 | .info_mask_separate = BIT(IIO_CHAN_INFO_PROCESSED), |
251 | }, |
252 | }; |
253 | |
254 | static int zopt2201_read_raw(struct iio_dev *indio_dev, |
255 | struct iio_chan_spec const *chan, |
256 | int *val, int *val2, long mask) |
257 | { |
258 | struct zopt2201_data *data = iio_priv(indio_dev); |
259 | u64 tmp; |
260 | int ret; |
261 | |
262 | switch (mask) { |
263 | case IIO_CHAN_INFO_RAW: |
264 | ret = zopt2201_read(data, reg: chan->address); |
265 | if (ret < 0) |
266 | return ret; |
267 | *val = ret; |
268 | return IIO_VAL_INT; |
269 | case IIO_CHAN_INFO_PROCESSED: |
270 | ret = zopt2201_read(data, ZOPT2201_UVB_DATA); |
271 | if (ret < 0) |
272 | return ret; |
273 | *val = ret * 18 * |
274 | (1 << (20 - zopt2201_resolution[data->res].bits)) / |
275 | zopt2201_gain_uvb[data->gain].gain; |
276 | return IIO_VAL_INT; |
277 | case IIO_CHAN_INFO_SCALE: |
278 | switch (chan->address) { |
279 | case ZOPT2201_ALS_DATA: |
280 | *val = zopt2201_gain_als[data->gain].scale; |
281 | break; |
282 | case ZOPT2201_UVB_DATA: |
283 | *val = zopt2201_gain_uvb[data->gain].scale; |
284 | break; |
285 | default: |
286 | return -EINVAL; |
287 | } |
288 | |
289 | *val2 = 1000000; |
290 | *val2 *= (1 << (zopt2201_resolution[data->res].bits - 13)); |
291 | tmp = div_s64(dividend: *val * 1000000ULL, divisor: *val2); |
292 | *val = div_s64_rem(dividend: tmp, divisor: 1000000, remainder: val2); |
293 | |
294 | return IIO_VAL_INT_PLUS_MICRO; |
295 | case IIO_CHAN_INFO_INT_TIME: |
296 | *val = 0; |
297 | *val2 = zopt2201_resolution[data->res].us; |
298 | return IIO_VAL_INT_PLUS_MICRO; |
299 | default: |
300 | return -EINVAL; |
301 | } |
302 | } |
303 | |
304 | static int zopt2201_set_resolution(struct zopt2201_data *data, u8 res) |
305 | { |
306 | int ret; |
307 | |
308 | ret = i2c_smbus_write_byte_data(client: data->client, ZOPT2201_LS_MEAS_RATE, |
309 | value: (res << ZOPT2201_MEAS_RES_SHIFT) | |
310 | data->rate); |
311 | if (ret < 0) |
312 | return ret; |
313 | |
314 | data->res = res; |
315 | |
316 | return 0; |
317 | } |
318 | |
319 | static int zopt2201_write_resolution(struct zopt2201_data *data, |
320 | int val, int val2) |
321 | { |
322 | int i, ret; |
323 | |
324 | if (val != 0) |
325 | return -EINVAL; |
326 | |
327 | for (i = 0; i < ARRAY_SIZE(zopt2201_resolution); i++) |
328 | if (val2 == zopt2201_resolution[i].us) { |
329 | mutex_lock(&data->lock); |
330 | ret = zopt2201_set_resolution(data, res: i); |
331 | mutex_unlock(lock: &data->lock); |
332 | return ret; |
333 | } |
334 | |
335 | return -EINVAL; |
336 | } |
337 | |
338 | static int zopt2201_set_gain(struct zopt2201_data *data, u8 gain) |
339 | { |
340 | int ret; |
341 | |
342 | ret = i2c_smbus_write_byte_data(client: data->client, ZOPT2201_LS_GAIN, value: gain); |
343 | if (ret < 0) |
344 | return ret; |
345 | |
346 | data->gain = gain; |
347 | |
348 | return 0; |
349 | } |
350 | |
351 | static int zopt2201_write_scale_als_by_idx(struct zopt2201_data *data, int idx) |
352 | { |
353 | int ret; |
354 | |
355 | mutex_lock(&data->lock); |
356 | ret = zopt2201_set_resolution(data, res: zopt2201_scale_als[idx].res); |
357 | if (ret < 0) |
358 | goto unlock; |
359 | |
360 | ret = zopt2201_set_gain(data, gain: zopt2201_scale_als[idx].gain); |
361 | |
362 | unlock: |
363 | mutex_unlock(lock: &data->lock); |
364 | return ret; |
365 | } |
366 | |
367 | static int zopt2201_write_scale_als(struct zopt2201_data *data, |
368 | int val, int val2) |
369 | { |
370 | int i; |
371 | |
372 | for (i = 0; i < ARRAY_SIZE(zopt2201_scale_als); i++) |
373 | if (val == zopt2201_scale_als[i].scale && |
374 | val2 == zopt2201_scale_als[i].uscale) { |
375 | return zopt2201_write_scale_als_by_idx(data, idx: i); |
376 | } |
377 | |
378 | return -EINVAL; |
379 | } |
380 | |
381 | static int zopt2201_write_scale_uvb_by_idx(struct zopt2201_data *data, int idx) |
382 | { |
383 | int ret; |
384 | |
385 | mutex_lock(&data->lock); |
386 | ret = zopt2201_set_resolution(data, res: zopt2201_scale_als[idx].res); |
387 | if (ret < 0) |
388 | goto unlock; |
389 | |
390 | ret = zopt2201_set_gain(data, gain: zopt2201_scale_als[idx].gain); |
391 | |
392 | unlock: |
393 | mutex_unlock(lock: &data->lock); |
394 | return ret; |
395 | } |
396 | |
397 | static int zopt2201_write_scale_uvb(struct zopt2201_data *data, |
398 | int val, int val2) |
399 | { |
400 | int i; |
401 | |
402 | for (i = 0; i < ARRAY_SIZE(zopt2201_scale_uvb); i++) |
403 | if (val == zopt2201_scale_uvb[i].scale && |
404 | val2 == zopt2201_scale_uvb[i].uscale) |
405 | return zopt2201_write_scale_uvb_by_idx(data, idx: i); |
406 | |
407 | return -EINVAL; |
408 | } |
409 | |
410 | static int zopt2201_write_raw(struct iio_dev *indio_dev, |
411 | struct iio_chan_spec const *chan, |
412 | int val, int val2, long mask) |
413 | { |
414 | struct zopt2201_data *data = iio_priv(indio_dev); |
415 | |
416 | switch (mask) { |
417 | case IIO_CHAN_INFO_INT_TIME: |
418 | return zopt2201_write_resolution(data, val, val2); |
419 | case IIO_CHAN_INFO_SCALE: |
420 | switch (chan->address) { |
421 | case ZOPT2201_ALS_DATA: |
422 | return zopt2201_write_scale_als(data, val, val2); |
423 | case ZOPT2201_UVB_DATA: |
424 | return zopt2201_write_scale_uvb(data, val, val2); |
425 | default: |
426 | return -EINVAL; |
427 | } |
428 | } |
429 | |
430 | return -EINVAL; |
431 | } |
432 | |
433 | static ssize_t zopt2201_show_int_time_available(struct device *dev, |
434 | struct device_attribute *attr, |
435 | char *buf) |
436 | { |
437 | size_t len = 0; |
438 | int i; |
439 | |
440 | for (i = 0; i < ARRAY_SIZE(zopt2201_resolution); i++) |
441 | len += scnprintf(buf: buf + len, PAGE_SIZE - len, fmt: "0.%06lu " , |
442 | zopt2201_resolution[i].us); |
443 | buf[len - 1] = '\n'; |
444 | |
445 | return len; |
446 | } |
447 | |
448 | static IIO_DEV_ATTR_INT_TIME_AVAIL(zopt2201_show_int_time_available); |
449 | |
450 | static ssize_t zopt2201_show_als_scale_avail(struct device *dev, |
451 | struct device_attribute *attr, |
452 | char *buf) |
453 | { |
454 | ssize_t len = 0; |
455 | int i; |
456 | |
457 | for (i = 0; i < ARRAY_SIZE(zopt2201_scale_als); i++) |
458 | len += scnprintf(buf: buf + len, PAGE_SIZE - len, fmt: "%d.%06u " , |
459 | zopt2201_scale_als[i].scale, |
460 | zopt2201_scale_als[i].uscale); |
461 | buf[len - 1] = '\n'; |
462 | |
463 | return len; |
464 | } |
465 | |
466 | static ssize_t zopt2201_show_uvb_scale_avail(struct device *dev, |
467 | struct device_attribute *attr, |
468 | char *buf) |
469 | { |
470 | ssize_t len = 0; |
471 | int i; |
472 | |
473 | for (i = 0; i < ARRAY_SIZE(zopt2201_scale_uvb); i++) |
474 | len += scnprintf(buf: buf + len, PAGE_SIZE - len, fmt: "%d.%06u " , |
475 | zopt2201_scale_uvb[i].scale, |
476 | zopt2201_scale_uvb[i].uscale); |
477 | buf[len - 1] = '\n'; |
478 | |
479 | return len; |
480 | } |
481 | |
482 | static IIO_DEVICE_ATTR(in_illuminance_scale_available, 0444, |
483 | zopt2201_show_als_scale_avail, NULL, 0); |
484 | static IIO_DEVICE_ATTR(in_intensity_uv_scale_available, 0444, |
485 | zopt2201_show_uvb_scale_avail, NULL, 0); |
486 | |
487 | static struct attribute *zopt2201_attributes[] = { |
488 | &iio_dev_attr_integration_time_available.dev_attr.attr, |
489 | &iio_dev_attr_in_illuminance_scale_available.dev_attr.attr, |
490 | &iio_dev_attr_in_intensity_uv_scale_available.dev_attr.attr, |
491 | NULL |
492 | }; |
493 | |
494 | static const struct attribute_group zopt2201_attribute_group = { |
495 | .attrs = zopt2201_attributes, |
496 | }; |
497 | |
498 | static const struct iio_info zopt2201_info = { |
499 | .read_raw = zopt2201_read_raw, |
500 | .write_raw = zopt2201_write_raw, |
501 | .attrs = &zopt2201_attribute_group, |
502 | }; |
503 | |
504 | static int zopt2201_probe(struct i2c_client *client) |
505 | { |
506 | struct zopt2201_data *data; |
507 | struct iio_dev *indio_dev; |
508 | int ret; |
509 | |
510 | if (!i2c_check_functionality(adap: client->adapter, |
511 | I2C_FUNC_SMBUS_READ_I2C_BLOCK)) |
512 | return -EOPNOTSUPP; |
513 | |
514 | ret = i2c_smbus_read_byte_data(client, ZOPT2201_PART_ID); |
515 | if (ret < 0) |
516 | return ret; |
517 | if (ret != ZOPT2201_PART_NUMBER) |
518 | return -ENODEV; |
519 | |
520 | indio_dev = devm_iio_device_alloc(parent: &client->dev, sizeof_priv: sizeof(*data)); |
521 | if (!indio_dev) |
522 | return -ENOMEM; |
523 | |
524 | data = iio_priv(indio_dev); |
525 | i2c_set_clientdata(client, data: indio_dev); |
526 | data->client = client; |
527 | mutex_init(&data->lock); |
528 | |
529 | indio_dev->info = &zopt2201_info; |
530 | indio_dev->channels = zopt2201_channels; |
531 | indio_dev->num_channels = ARRAY_SIZE(zopt2201_channels); |
532 | indio_dev->name = ZOPT2201_DRV_NAME; |
533 | indio_dev->modes = INDIO_DIRECT_MODE; |
534 | |
535 | data->rate = ZOPT2201_MEAS_FREQ_100MS; |
536 | ret = zopt2201_set_resolution(data, ZOPT2201_MEAS_RES_18BIT); |
537 | if (ret < 0) |
538 | return ret; |
539 | |
540 | ret = zopt2201_set_gain(data, ZOPT2201_LS_GAIN_3); |
541 | if (ret < 0) |
542 | return ret; |
543 | |
544 | return devm_iio_device_register(&client->dev, indio_dev); |
545 | } |
546 | |
547 | static const struct i2c_device_id zopt2201_id[] = { |
548 | { "zopt2201" , 0 }, |
549 | { } |
550 | }; |
551 | MODULE_DEVICE_TABLE(i2c, zopt2201_id); |
552 | |
553 | static struct i2c_driver zopt2201_driver = { |
554 | .driver = { |
555 | .name = ZOPT2201_DRV_NAME, |
556 | }, |
557 | .probe = zopt2201_probe, |
558 | .id_table = zopt2201_id, |
559 | }; |
560 | |
561 | module_i2c_driver(zopt2201_driver); |
562 | |
563 | MODULE_AUTHOR("Peter Meerwald-Stadler <pmeerw@pmeerw.net>" ); |
564 | MODULE_DESCRIPTION("IDT ZOPT2201 ambient light and UV B sensor driver" ); |
565 | MODULE_LICENSE("GPL" ); |
566 | |