xref: /freebsd/sys/dev/usb/gadget/g_audio.c (revision 61e21613)
1 /*-
2  * SPDX-License-Identifier: BSD-2-Clause
3  *
4  * Copyright (c) 2010 Hans Petter Selasky. All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions
8  * are met:
9  * 1. Redistributions of source code must retain the above copyright
10  *    notice, this list of conditions and the following disclaimer.
11  * 2. Redistributions in binary form must reproduce the above copyright
12  *    notice, this list of conditions and the following disclaimer in the
13  *    documentation and/or other materials provided with the distribution.
14  *
15  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
16  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
19  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
21  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
22  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
23  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25  * SUCH DAMAGE.
26  */
27 
28 /*
29  * USB audio specs: http://www.usb.org/developers/devclass_docs/audio10.pdf
30  *		    http://www.usb.org/developers/devclass_docs/frmts10.pdf
31  *		    http://www.usb.org/developers/devclass_docs/termt10.pdf
32  */
33 
34 #include <sys/param.h>
35 #include <sys/stdint.h>
36 #include <sys/stddef.h>
37 #include <sys/queue.h>
38 #include <sys/systm.h>
39 #include <sys/kernel.h>
40 #include <sys/bus.h>
41 #include <sys/linker_set.h>
42 #include <sys/module.h>
43 #include <sys/lock.h>
44 #include <sys/mutex.h>
45 #include <sys/condvar.h>
46 #include <sys/sysctl.h>
47 #include <sys/sx.h>
48 #include <sys/unistd.h>
49 #include <sys/callout.h>
50 #include <sys/malloc.h>
51 #include <sys/priv.h>
52 
53 #include <dev/usb/usb.h>
54 #include <dev/usb/usb_cdc.h>
55 #include <dev/usb/usbdi.h>
56 #include <dev/usb/usbdi_util.h>
57 #include <dev/usb/usbhid.h>
58 #include "usb_if.h"
59 
60 #define	USB_DEBUG_VAR g_audio_debug
61 #include <dev/usb/usb_debug.h>
62 
63 #include <dev/usb/gadget/g_audio.h>
64 
65 enum {
66 	G_AUDIO_ISOC0_RD,
67 	G_AUDIO_ISOC1_RD,
68 	G_AUDIO_ISOC0_WR,
69 	G_AUDIO_ISOC1_WR,
70 	G_AUDIO_N_TRANSFER,
71 };
72 
73 struct g_audio_softc {
74 	struct mtx sc_mtx;
75 	struct usb_callout sc_callout;
76 	struct usb_callout sc_watchdog;
77 	struct usb_xfer *sc_xfer[G_AUDIO_N_TRANSFER];
78 
79 	int	sc_mode;
80 	int	sc_pattern_len;
81 	int	sc_throughput;
82 	int	sc_tx_interval;
83 	int	sc_state;
84 	int	sc_noise_rem;
85 
86 	int8_t	sc_pattern[G_AUDIO_MAX_STRLEN];
87 
88 	uint16_t sc_data_len[2][G_AUDIO_FRAMES];
89 
90 	int16_t	sc_data_buf[2][G_AUDIO_BUFSIZE / 2];
91 
92 	uint8_t	sc_volume_setting[32];
93 	uint8_t	sc_volume_limit[32];
94 	uint8_t	sc_sample_rate[32];
95 };
96 
97 static SYSCTL_NODE(_hw_usb, OID_AUTO, g_audio, CTLFLAG_RW | CTLFLAG_MPSAFE, 0,
98     "USB audio gadget");
99 
100 #ifdef USB_DEBUG
101 static int g_audio_debug = 0;
102 
103 SYSCTL_INT(_hw_usb_g_audio, OID_AUTO, debug, CTLFLAG_RWTUN,
104     &g_audio_debug, 0, "Debug level");
105 #endif
106 
107 static int g_audio_mode = 0;
108 
109 SYSCTL_INT(_hw_usb_g_audio, OID_AUTO, mode, CTLFLAG_RWTUN,
110     &g_audio_mode, 0, "Mode selection");
111 
112 static int g_audio_pattern_interval = 1000;
113 
114 SYSCTL_INT(_hw_usb_g_audio, OID_AUTO, pattern_interval, CTLFLAG_RWTUN,
115     &g_audio_pattern_interval, 0, "Pattern interval in milliseconds");
116 
117 static char g_audio_pattern_data[G_AUDIO_MAX_STRLEN];
118 
119 SYSCTL_STRING(_hw_usb_g_audio, OID_AUTO, pattern, CTLFLAG_RW,
120     &g_audio_pattern_data, sizeof(g_audio_pattern_data), "Data pattern");
121 
122 static int g_audio_throughput;
123 
124 SYSCTL_INT(_hw_usb_g_audio, OID_AUTO, throughput, CTLFLAG_RD,
125     &g_audio_throughput, sizeof(g_audio_throughput), "Throughput in bytes per second");
126 
127 static device_probe_t g_audio_probe;
128 static device_attach_t g_audio_attach;
129 static device_detach_t g_audio_detach;
130 static usb_handle_request_t g_audio_handle_request;
131 
132 static usb_callback_t g_audio_isoc_read_callback;
133 static usb_callback_t g_audio_isoc_write_callback;
134 
135 static void g_audio_watchdog(void *arg);
136 static void g_audio_timeout(void *arg);
137 
138 static device_method_t g_audio_methods[] = {
139 	/* USB interface */
140 	DEVMETHOD(usb_handle_request, g_audio_handle_request),
141 
142 	/* Device interface */
143 	DEVMETHOD(device_probe, g_audio_probe),
144 	DEVMETHOD(device_attach, g_audio_attach),
145 	DEVMETHOD(device_detach, g_audio_detach),
146 
147 	DEVMETHOD_END
148 };
149 
150 static driver_t g_audio_driver = {
151 	.name = "g_audio",
152 	.methods = g_audio_methods,
153 	.size = sizeof(struct g_audio_softc),
154 };
155 
156 DRIVER_MODULE(g_audio, uhub, g_audio_driver, 0, 0);
157 MODULE_DEPEND(g_audio, usb, 1, 1, 1);
158 
159 static const struct usb_config g_audio_config[G_AUDIO_N_TRANSFER] = {
160 	[G_AUDIO_ISOC0_RD] = {
161 		.type = UE_ISOCHRONOUS,
162 		.endpoint = UE_ADDR_ANY,
163 		.direction = UE_DIR_RX,
164 		.flags = {.ext_buffer = 1,.pipe_bof = 1,.short_xfer_ok = 1,},
165 		.bufsize = G_AUDIO_BUFSIZE,
166 		.callback = &g_audio_isoc_read_callback,
167 		.frames = G_AUDIO_FRAMES,
168 		.usb_mode = USB_MODE_DEVICE,
169 		.if_index = 1,
170 	},
171 
172 	[G_AUDIO_ISOC1_RD] = {
173 		.type = UE_ISOCHRONOUS,
174 		.endpoint = UE_ADDR_ANY,
175 		.direction = UE_DIR_RX,
176 		.flags = {.ext_buffer = 1,.pipe_bof = 1,.short_xfer_ok = 1,},
177 		.bufsize = G_AUDIO_BUFSIZE,
178 		.callback = &g_audio_isoc_read_callback,
179 		.frames = G_AUDIO_FRAMES,
180 		.usb_mode = USB_MODE_DEVICE,
181 		.if_index = 1,
182 	},
183 
184 	[G_AUDIO_ISOC0_WR] = {
185 		.type = UE_ISOCHRONOUS,
186 		.endpoint = UE_ADDR_ANY,
187 		.direction = UE_DIR_TX,
188 		.flags = {.ext_buffer = 1,.pipe_bof = 1,},
189 		.bufsize = G_AUDIO_BUFSIZE,
190 		.callback = &g_audio_isoc_write_callback,
191 		.frames = G_AUDIO_FRAMES,
192 		.usb_mode = USB_MODE_DEVICE,
193 		.if_index = 2,
194 	},
195 
196 	[G_AUDIO_ISOC1_WR] = {
197 		.type = UE_ISOCHRONOUS,
198 		.endpoint = UE_ADDR_ANY,
199 		.direction = UE_DIR_TX,
200 		.flags = {.ext_buffer = 1,.pipe_bof = 1,},
201 		.bufsize = G_AUDIO_BUFSIZE,
202 		.callback = &g_audio_isoc_write_callback,
203 		.frames = G_AUDIO_FRAMES,
204 		.usb_mode = USB_MODE_DEVICE,
205 		.if_index = 2,
206 	},
207 };
208 
209 static void
210 g_audio_timeout_reset(struct g_audio_softc *sc)
211 {
212 	int i = g_audio_pattern_interval;
213 
214 	sc->sc_tx_interval = i;
215 
216 	if (i <= 0)
217 		i = 1;
218 	else if (i > 1023)
219 		i = 1023;
220 
221 	i = USB_MS_TO_TICKS(i);
222 
223 	usb_callout_reset(&sc->sc_callout, i, &g_audio_timeout, sc);
224 }
225 
226 static void
227 g_audio_timeout(void *arg)
228 {
229 	struct g_audio_softc *sc = arg;
230 
231 	sc->sc_mode = g_audio_mode;
232 
233 	memcpy(sc->sc_pattern, g_audio_pattern_data, sizeof(sc->sc_pattern));
234 
235 	sc->sc_pattern[G_AUDIO_MAX_STRLEN - 1] = 0;
236 
237 	sc->sc_pattern_len = strlen(sc->sc_pattern);
238 
239 	if (sc->sc_mode != G_AUDIO_MODE_LOOP) {
240 		usbd_transfer_start(sc->sc_xfer[G_AUDIO_ISOC0_WR]);
241 		usbd_transfer_start(sc->sc_xfer[G_AUDIO_ISOC1_WR]);
242 	}
243 	g_audio_timeout_reset(sc);
244 }
245 
246 static void
247 g_audio_watchdog_reset(struct g_audio_softc *sc)
248 {
249 	usb_callout_reset(&sc->sc_watchdog, hz, &g_audio_watchdog, sc);
250 }
251 
252 static void
253 g_audio_watchdog(void *arg)
254 {
255 	struct g_audio_softc *sc = arg;
256 	int i;
257 
258 	i = sc->sc_throughput;
259 
260 	sc->sc_throughput = 0;
261 
262 	g_audio_throughput = i;
263 
264 	g_audio_watchdog_reset(sc);
265 }
266 
267 static int
268 g_audio_probe(device_t dev)
269 {
270 	struct usb_attach_arg *uaa = device_get_ivars(dev);
271 
272 	DPRINTFN(11, "\n");
273 
274 	if (uaa->usb_mode != USB_MODE_DEVICE)
275 		return (ENXIO);
276 
277 	if ((uaa->info.bInterfaceClass == UICLASS_AUDIO) &&
278 	    (uaa->info.bInterfaceSubClass == UISUBCLASS_AUDIOCONTROL))
279 		return (0);
280 
281 	return (ENXIO);
282 }
283 
284 static int
285 g_audio_attach(device_t dev)
286 {
287 	struct g_audio_softc *sc = device_get_softc(dev);
288 	struct usb_attach_arg *uaa = device_get_ivars(dev);
289 	int error;
290 	int i;
291 	uint8_t iface_index[3];
292 
293 	DPRINTFN(11, "\n");
294 
295 	device_set_usb_desc(dev);
296 
297 	mtx_init(&sc->sc_mtx, "g_audio", NULL, MTX_DEF);
298 
299 	usb_callout_init_mtx(&sc->sc_callout, &sc->sc_mtx, 0);
300 	usb_callout_init_mtx(&sc->sc_watchdog, &sc->sc_mtx, 0);
301 
302 	sc->sc_mode = G_AUDIO_MODE_SILENT;
303 
304 	sc->sc_noise_rem = 1;
305 
306 	for (i = 0; i != G_AUDIO_FRAMES; i++) {
307 		sc->sc_data_len[0][i] = G_AUDIO_BUFSIZE / G_AUDIO_FRAMES;
308 		sc->sc_data_len[1][i] = G_AUDIO_BUFSIZE / G_AUDIO_FRAMES;
309 	}
310 
311 	iface_index[0] = uaa->info.bIfaceIndex;
312 	iface_index[1] = uaa->info.bIfaceIndex + 1;
313 	iface_index[2] = uaa->info.bIfaceIndex + 2;
314 
315 	error = usbd_set_alt_interface_index(uaa->device, iface_index[1], 1);
316 	if (error) {
317 		DPRINTF("alt iface setting error=%s\n", usbd_errstr(error));
318 		goto detach;
319 	}
320 	error = usbd_set_alt_interface_index(uaa->device, iface_index[2], 1);
321 	if (error) {
322 		DPRINTF("alt iface setting error=%s\n", usbd_errstr(error));
323 		goto detach;
324 	}
325 	error = usbd_transfer_setup(uaa->device,
326 	    iface_index, sc->sc_xfer, g_audio_config,
327 	    G_AUDIO_N_TRANSFER, sc, &sc->sc_mtx);
328 
329 	if (error) {
330 		DPRINTF("error=%s\n", usbd_errstr(error));
331 		goto detach;
332 	}
333 	usbd_set_parent_iface(uaa->device, iface_index[1], iface_index[0]);
334 	usbd_set_parent_iface(uaa->device, iface_index[2], iface_index[0]);
335 
336 	mtx_lock(&sc->sc_mtx);
337 
338 	usbd_transfer_start(sc->sc_xfer[G_AUDIO_ISOC0_RD]);
339 	usbd_transfer_start(sc->sc_xfer[G_AUDIO_ISOC1_RD]);
340 
341 	usbd_transfer_start(sc->sc_xfer[G_AUDIO_ISOC0_WR]);
342 	usbd_transfer_start(sc->sc_xfer[G_AUDIO_ISOC1_WR]);
343 
344 	g_audio_timeout_reset(sc);
345 
346 	g_audio_watchdog_reset(sc);
347 
348 	mtx_unlock(&sc->sc_mtx);
349 
350 	return (0);			/* success */
351 
352 detach:
353 	g_audio_detach(dev);
354 
355 	return (ENXIO);			/* error */
356 }
357 
358 static int
359 g_audio_detach(device_t dev)
360 {
361 	struct g_audio_softc *sc = device_get_softc(dev);
362 
363 	DPRINTF("\n");
364 
365 	mtx_lock(&sc->sc_mtx);
366 	usb_callout_stop(&sc->sc_callout);
367 	usb_callout_stop(&sc->sc_watchdog);
368 	mtx_unlock(&sc->sc_mtx);
369 
370 	usbd_transfer_unsetup(sc->sc_xfer, G_AUDIO_N_TRANSFER);
371 
372 	usb_callout_drain(&sc->sc_callout);
373 	usb_callout_drain(&sc->sc_watchdog);
374 
375 	mtx_destroy(&sc->sc_mtx);
376 
377 	return (0);
378 }
379 
380 static int32_t
381 g_noise(struct g_audio_softc *sc)
382 {
383 	uint32_t temp;
384 	const uint32_t prime = 0xFFFF1D;
385 
386 	if (sc->sc_noise_rem & 1) {
387 		sc->sc_noise_rem += prime;
388 	}
389 	sc->sc_noise_rem /= 2;
390 
391 	temp = sc->sc_noise_rem;
392 
393 	/* unsigned to signed conversion */
394 
395 	temp ^= 0x800000;
396 	if (temp & 0x800000) {
397 		temp |= (-0x800000);
398 	}
399 	return temp;
400 }
401 
402 static void
403 g_audio_make_samples(struct g_audio_softc *sc, int16_t *ptr, int samples)
404 {
405 	int i;
406 	int j;
407 
408 	for (i = 0; i != samples; i++) {
409 		j = g_noise(sc);
410 
411 		if ((sc->sc_state < 0) || (sc->sc_state >= sc->sc_pattern_len))
412 			sc->sc_state = 0;
413 
414 		if (sc->sc_pattern_len != 0) {
415 			j = (j * sc->sc_pattern[sc->sc_state]) >> 16;
416 			sc->sc_state++;
417 		}
418 		*ptr++ = j / 256;
419 		*ptr++ = j / 256;
420 	}
421 }
422 
423 static void
424 g_audio_isoc_write_callback(struct usb_xfer *xfer, usb_error_t error)
425 {
426 	struct g_audio_softc *sc = usbd_xfer_softc(xfer);
427 	int actlen;
428 	int aframes;
429 	int nr = (xfer == sc->sc_xfer[G_AUDIO_ISOC0_WR]) ? 0 : 1;
430 	int16_t *ptr;
431 	int i;
432 
433 	usbd_xfer_status(xfer, &actlen, NULL, &aframes, NULL);
434 
435 	DPRINTF("st=%d aframes=%d actlen=%d bytes\n",
436 	    USB_GET_STATE(xfer), aframes, actlen);
437 
438 	switch (USB_GET_STATE(xfer)) {
439 	case USB_ST_TRANSFERRED:
440 
441 		sc->sc_throughput += actlen;
442 
443 		if (sc->sc_mode == G_AUDIO_MODE_LOOP)
444 			break;		/* sync with RX */
445 
446 	case USB_ST_SETUP:
447 tr_setup:
448 
449 		ptr = sc->sc_data_buf[nr];
450 
451 		if (sc->sc_mode == G_AUDIO_MODE_PATTERN) {
452 			for (i = 0; i != G_AUDIO_FRAMES; i++) {
453 				usbd_xfer_set_frame_data(xfer, i, ptr, sc->sc_data_len[nr][i]);
454 
455 				g_audio_make_samples(sc, ptr, (G_AUDIO_BUFSIZE / G_AUDIO_FRAMES) / 2);
456 
457 				ptr += (G_AUDIO_BUFSIZE / G_AUDIO_FRAMES) / 2;
458 			}
459 		} else if (sc->sc_mode == G_AUDIO_MODE_LOOP) {
460 			for (i = 0; i != G_AUDIO_FRAMES; i++) {
461 				usbd_xfer_set_frame_data(xfer, i, ptr, sc->sc_data_len[nr][i] & ~3);
462 
463 				g_audio_make_samples(sc, ptr, sc->sc_data_len[nr][i] / 4);
464 
465 				ptr += (G_AUDIO_BUFSIZE / G_AUDIO_FRAMES) / 2;
466 			}
467 		}
468 		break;
469 
470 	default:			/* Error */
471 		DPRINTF("error=%s\n", usbd_errstr(error));
472 
473 		if (error != USB_ERR_CANCELLED) {
474 			/* try to clear stall first */
475 			usbd_xfer_set_stall(xfer);
476 			goto tr_setup;
477 		}
478 		break;
479 	}
480 }
481 
482 static void
483 g_audio_isoc_read_callback(struct usb_xfer *xfer, usb_error_t error)
484 {
485 	struct g_audio_softc *sc = usbd_xfer_softc(xfer);
486 	int actlen;
487 	int aframes;
488 	int nr = (xfer == sc->sc_xfer[G_AUDIO_ISOC0_RD]) ? 0 : 1;
489 	int16_t *ptr;
490 	int i;
491 
492 	usbd_xfer_status(xfer, &actlen, NULL, &aframes, NULL);
493 
494 	DPRINTF("st=%d aframes=%d actlen=%d bytes\n",
495 	    USB_GET_STATE(xfer), aframes, actlen);
496 
497 	switch (USB_GET_STATE(xfer)) {
498 	case USB_ST_TRANSFERRED:
499 
500 		sc->sc_throughput += actlen;
501 
502 		for (i = 0; i != G_AUDIO_FRAMES; i++) {
503 			sc->sc_data_len[nr][i] = usbd_xfer_frame_len(xfer, i);
504 		}
505 
506 		usbd_transfer_start(sc->sc_xfer[G_AUDIO_ISOC0_WR]);
507 		usbd_transfer_start(sc->sc_xfer[G_AUDIO_ISOC1_WR]);
508 
509 		break;
510 
511 	case USB_ST_SETUP:
512 tr_setup:
513 		ptr = sc->sc_data_buf[nr];
514 
515 		for (i = 0; i != G_AUDIO_FRAMES; i++) {
516 			usbd_xfer_set_frame_data(xfer, i, ptr,
517 			    G_AUDIO_BUFSIZE / G_AUDIO_FRAMES);
518 
519 			ptr += (G_AUDIO_BUFSIZE / G_AUDIO_FRAMES) / 2;
520 		}
521 
522 		usbd_transfer_submit(xfer);
523 		break;
524 
525 	default:			/* Error */
526 		DPRINTF("error=%s\n", usbd_errstr(error));
527 
528 		if (error != USB_ERR_CANCELLED) {
529 			/* try to clear stall first */
530 			usbd_xfer_set_stall(xfer);
531 			goto tr_setup;
532 		}
533 		break;
534 	}
535 }
536 
537 static int
538 g_audio_handle_request(device_t dev,
539     const void *preq, void **pptr, uint16_t *plen,
540     uint16_t offset, uint8_t *pstate)
541 {
542 	struct g_audio_softc *sc = device_get_softc(dev);
543 	const struct usb_device_request *req = preq;
544 	uint8_t is_complete = *pstate;
545 
546 	if (!is_complete) {
547 		if ((req->bmRequestType == UT_READ_CLASS_INTERFACE) &&
548 		    (req->bRequest == 0x82 /* get min */ )) {
549 			if (offset == 0) {
550 				USETW(sc->sc_volume_limit, 0);
551 				*plen = 2;
552 				*pptr = &sc->sc_volume_limit;
553 			} else {
554 				*plen = 0;
555 			}
556 			return (0);
557 		} else if ((req->bmRequestType == UT_READ_CLASS_INTERFACE) &&
558 		    (req->bRequest == 0x83 /* get max */ )) {
559 			if (offset == 0) {
560 				USETW(sc->sc_volume_limit, 0x2000);
561 				*plen = 2;
562 				*pptr = &sc->sc_volume_limit;
563 			} else {
564 				*plen = 0;
565 			}
566 			return (0);
567 		} else if ((req->bmRequestType == UT_READ_CLASS_INTERFACE) &&
568 		    (req->bRequest == 0x84 /* get residue */ )) {
569 			if (offset == 0) {
570 				USETW(sc->sc_volume_limit, 1);
571 				*plen = 2;
572 				*pptr = &sc->sc_volume_limit;
573 			} else {
574 				*plen = 0;
575 			}
576 			return (0);
577 		} else if ((req->bmRequestType == UT_READ_CLASS_INTERFACE) &&
578 		    (req->bRequest == 0x81 /* get value */ )) {
579 			if (offset == 0) {
580 				USETW(sc->sc_volume_setting, 0x2000);
581 				*plen = sizeof(sc->sc_volume_setting);
582 				*pptr = &sc->sc_volume_setting;
583 			} else {
584 				*plen = 0;
585 			}
586 			return (0);
587 		} else if ((req->bmRequestType == UT_WRITE_CLASS_INTERFACE) &&
588 		    (req->bRequest == 0x01 /* set value */ )) {
589 			if (offset == 0) {
590 				*plen = sizeof(sc->sc_volume_setting);
591 				*pptr = &sc->sc_volume_setting;
592 			} else {
593 				*plen = 0;
594 			}
595 			return (0);
596 		} else if ((req->bmRequestType == UT_WRITE_CLASS_ENDPOINT) &&
597 		    (req->bRequest == 0x01 /* set value */ )) {
598 			if (offset == 0) {
599 				*plen = sizeof(sc->sc_sample_rate);
600 				*pptr = &sc->sc_sample_rate;
601 			} else {
602 				*plen = 0;
603 			}
604 			return (0);
605 		}
606 	}
607 	return (ENXIO);			/* use builtin handler */
608 }
609