xref: /freebsd/sys/dev/acpi_support/acpi_asus.c (revision 6b3455a7665208c366849f0b2b3bc916fb97516e)
1 /*-
2  * Copyright (c) 2004 Philip Paeps <philip@FreeBSD.org>
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
15  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
17  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
18  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
20  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
21  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
22  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
23  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
24  * SUCH DAMAGE.
25  *
26  */
27 
28 #include <sys/cdefs.h>
29 __FBSDID("$FreeBSD$");
30 
31 /*
32  * Driver for extra ACPI-controlled gadgets (hotkeys, leds, etc) found on
33  * recent Asus (and Medion) laptops.  Inspired by the Acpi4Asus project which
34  * implements these features in the Linux kernel.
35  *
36  *   <http://sourceforge.net/projects/acpi4asus/>
37  *
38  * Currently should support most features, but could use some more testing.
39  * Particularly the display-switching stuff is a bit hairy.  If you have an
40  * Asus laptop which doesn't appear to be supported, or strange things happen
41  * when using this driver, please report to <acpi@FreeBSD.org>.
42  *
43  */
44 
45 #include "opt_acpi.h"
46 #include <sys/param.h>
47 #include <sys/kernel.h>
48 #include <sys/module.h>
49 #include <sys/bus.h>
50 #include <sys/sbuf.h>
51 
52 #include "acpi.h"
53 #include <dev/acpica/acpivar.h>
54 #include <dev/led/led.h>
55 
56 #define _COMPONENT	ACPI_ASUS
57 ACPI_MODULE_NAME("ASUS")
58 
59 struct acpi_asus_model {
60 	char	*name;
61 
62 	char	*mled_set;
63 	char	*tled_set;
64 	char	*wled_set;
65 
66 	char	*brn_get;
67 	char	*brn_set;
68 	char	*brn_up;
69 	char	*brn_dn;
70 
71 	char	*lcd_get;
72 	char	*lcd_set;
73 
74 	char	*disp_get;
75 	char	*disp_set;
76 };
77 
78 struct acpi_asus_softc {
79 	device_t		dev;
80 	ACPI_HANDLE		handle;
81 
82 	struct acpi_asus_model	*model;
83 	struct sysctl_ctx_list	sysctl_ctx;
84 	struct sysctl_oid	*sysctl_tree;
85 
86 	struct cdev *s_mled;
87 	struct cdev *s_tled;
88 	struct cdev *s_wled;
89 
90 	int			s_brn;
91 	int			s_disp;
92 	int			s_lcd;
93 };
94 
95 /* Models we know about */
96 static struct acpi_asus_model acpi_asus_models[] = {
97 	{
98 		.name		= "L2D",
99 		.mled_set	= "MLED",
100 		.wled_set	= "WLED",
101 		.brn_up		= "\\Q0E",
102 		.brn_dn		= "\\Q0F",
103 		.lcd_get	= "\\SGP0",
104 		.lcd_set	= "\\Q10"
105 	},
106 	{
107 		.name		= "L3C",
108 		.mled_set	= "MLED",
109 		.wled_set	= "WLED",
110 		.brn_get	= "GPLV",
111 		.brn_set	= "SPLV",
112 		.lcd_get	= "\\GL32",
113 		.lcd_set	= "\\_SB.PCI0.PX40.ECD0._Q10"
114 	},
115 	{
116 		.name		= "L3D",
117 		.mled_set	= "MLED",
118 		.wled_set	= "WLED",
119 		.brn_get	= "GPLV",
120 		.brn_set	= "SPLV",
121 		.lcd_get	= "\\BKLG",
122 		.lcd_set	= "\\Q10"
123 	},
124 	{
125 		.name		= "L3H",
126 		.mled_set	= "MLED",
127 		.wled_set	= "WLED",
128 		.brn_get	= "GPLV",
129 		.brn_set	= "SPLV",
130 		.lcd_get	= "\\_SB.PCI0.PM.PBC",
131 		.lcd_set	= "EHK",
132 		.disp_get	= "\\_SB.INFB",
133 		.disp_set	= "SDSP"
134 	},
135 	{
136 		.name		= "L8L"
137 		/* Only has hotkeys, apparantly */
138 	},
139 	{
140 		.name		= "M1A",
141 		.mled_set	= "MLED",
142 		.brn_up		= "\\_SB.PCI0.PX40.EC0.Q0E",
143 		.brn_dn		= "\\_SB.PCI0.PX40.EC0.Q0F",
144 		.lcd_get	= "\\PNOF",
145 		.lcd_set	= "\\_SB.PCI0.PX40.EC0.Q10"
146 	},
147 	{
148 		.name		= "M2E",
149 		.mled_set	= "MLED",
150 		.wled_set	= "WLED",
151 		.brn_get	= "GPLV",
152 		.brn_set	= "SPLV",
153 		.lcd_get	= "\\GP06",
154 		.lcd_set	= "\\Q10"
155 	},
156 	{
157 		.name		= "P30",
158 		.wled_set	= "WLED",
159 		.brn_up		= "\\_SB.PCI0.LPCB.EC0._Q68",
160 		.brn_dn		= "\\_SB.PCI0.LPCB.EC0._Q69",
161 		.lcd_get	= "\\BKLT",
162 		.lcd_set	= "\\_SB.PCI0.LPCB.EC0._Q0E"
163 	},
164 
165 	{ .name = NULL }
166 };
167 
168 /* Function prototypes */
169 static int	acpi_asus_probe(device_t dev);
170 static int	acpi_asus_attach(device_t dev);
171 static int	acpi_asus_detach(device_t dev);
172 
173 static void	acpi_asus_mled(device_t dev, int state);
174 static void	acpi_asus_tled(device_t dev, int state);
175 static void	acpi_asus_wled(device_t dev, int state);
176 
177 static int	acpi_asus_sysctl_brn(SYSCTL_HANDLER_ARGS);
178 static int	acpi_asus_sysctl_lcd(SYSCTL_HANDLER_ARGS);
179 static int	acpi_asus_sysctl_disp(SYSCTL_HANDLER_ARGS);
180 
181 static void	acpi_asus_notify(ACPI_HANDLE h, UINT32 notify, void *context);
182 
183 static device_method_t acpi_asus_methods[] = {
184 	DEVMETHOD(device_probe,	 acpi_asus_probe),
185 	DEVMETHOD(device_attach, acpi_asus_attach),
186 	DEVMETHOD(device_detach, acpi_asus_detach),
187 
188 	{ 0, 0 }
189 };
190 
191 static driver_t acpi_asus_driver = {
192 	"acpi_asus",
193 	acpi_asus_methods,
194 	sizeof(struct acpi_asus_softc)
195 };
196 
197 static devclass_t acpi_asus_devclass;
198 
199 DRIVER_MODULE(acpi_asus, acpi, acpi_asus_driver, acpi_asus_devclass, 0, 0);
200 MODULE_DEPEND(acpi_asus, acpi, 1, 1, 1);
201 
202 static int
203 acpi_asus_probe(device_t dev)
204 {
205 	struct acpi_asus_model	*model;
206 	struct acpi_asus_softc	*sc;
207 	struct sbuf		*sb;
208 	ACPI_BUFFER		Buf;
209 	ACPI_OBJECT		Arg, *Obj;
210 	ACPI_OBJECT_LIST	Args;
211 	static char 		*asus_ids[] = { "ATK0100", NULL };
212 
213 	ACPI_FUNCTION_TRACE((char *)(uintptr_t)__func__);
214 
215 	if (!acpi_disabled("asus") &&
216 	    ACPI_ID_PROBE(device_get_parent(dev), dev, asus_ids)) {
217 		sc = device_get_softc(dev);
218 		sc->dev = dev;
219 		sc->handle = acpi_get_handle(dev);
220 
221 		sb = sbuf_new(NULL, NULL, 0, SBUF_AUTOEXTEND);
222 
223 		if (sb == NULL)
224 			return (ENOMEM);
225 
226 		Arg.Type = ACPI_TYPE_INTEGER;
227 		Arg.Integer.Value = 0;
228 
229 		Args.Count = 1;
230 		Args.Pointer = &Arg;
231 
232 		Buf.Pointer = NULL;
233 		Buf.Length = ACPI_ALLOCATE_BUFFER;
234 
235 		AcpiEvaluateObject(sc->handle, "INIT", &Args, &Buf);
236 
237 		Obj = Buf.Pointer;
238 
239 		for (model = acpi_asus_models; model->name != NULL; model++)
240 			if (strcmp(Obj->String.Pointer, model->name) == 0) {
241 				sbuf_printf(sb, "Asus %s Laptop Extras",
242 						Obj->String.Pointer);
243 				sbuf_finish(sb);
244 
245 				sc->model = model;
246 				device_set_desc(dev, sbuf_data(sb));
247 
248 				sbuf_delete(sb);
249 				AcpiOsFree(Buf.Pointer);
250 				return (0);
251 			}
252 
253 		sbuf_printf(sb, "Unsupported Asus laptop detected: %s\n",
254 				Obj->String.Pointer);
255 		sbuf_finish(sb);
256 
257 		device_printf(dev, sbuf_data(sb));
258 
259 		sbuf_delete(sb);
260 		AcpiOsFree(Buf.Pointer);
261 	}
262 
263 	return (ENXIO);
264 }
265 
266 static int
267 acpi_asus_attach(device_t dev)
268 {
269 	struct acpi_asus_softc	*sc;
270 	struct acpi_softc	*acpi_sc;
271 
272 	ACPI_FUNCTION_TRACE((char *)(uintptr_t)__func__);
273 
274 	sc = device_get_softc(dev);
275 	acpi_sc = acpi_device_get_parent_softc(dev);
276 
277 	/* Build sysctl tree */
278 	sysctl_ctx_init(&sc->sysctl_ctx);
279 	sc->sysctl_tree = SYSCTL_ADD_NODE(&sc->sysctl_ctx,
280 	    SYSCTL_CHILDREN(acpi_sc->acpi_sysctl_tree),
281 	    OID_AUTO, "asus", CTLFLAG_RD, 0, "");
282 
283 	/* Attach leds */
284 	if (sc->model->mled_set)
285 		sc->s_mled = led_create((led_t *)acpi_asus_mled, dev, "mled");
286 
287 	if (sc->model->tled_set)
288 		sc->s_tled = led_create((led_t *)acpi_asus_tled, dev, "tled");
289 
290 	if (sc->model->wled_set)
291 		sc->s_wled = led_create((led_t *)acpi_asus_wled, dev, "wled");
292 
293 	/* Attach brightness for GPLV/SPLV models */
294 	if (sc->model->brn_get && ACPI_SUCCESS(acpi_GetInteger(sc->handle,
295 	    sc->model->brn_get, &sc->s_brn)))
296 		SYSCTL_ADD_PROC(&sc->sysctl_ctx,
297 		    SYSCTL_CHILDREN(sc->sysctl_tree), OID_AUTO,
298 		    "lcd_brightness", CTLTYPE_INT | CTLFLAG_RW, sc, 0,
299 		    acpi_asus_sysctl_brn, "I", "brightness of the lcd panel");
300 
301 	/* Attach brightness for other models */
302 	if (sc->model->brn_up &&
303 	    ACPI_SUCCESS(AcpiEvaluateObject(sc->handle, sc->model->brn_up,
304 	    NULL, NULL)) &&
305 	    ACPI_SUCCESS(AcpiEvaluateObject(sc->handle, sc->model->brn_dn,
306 	    NULL, NULL)))
307 		SYSCTL_ADD_PROC(&sc->sysctl_ctx,
308 		    SYSCTL_CHILDREN(sc->sysctl_tree), OID_AUTO,
309 		    "lcd_brightness", CTLTYPE_INT | CTLFLAG_RW, sc, 0,
310 		    acpi_asus_sysctl_brn, "I", "brightness of the lcd panel");
311 
312 	/* Attach display switching */
313 	if (sc->model->disp_get && ACPI_SUCCESS(acpi_GetInteger(sc->handle,
314 	    sc->model->disp_get, &sc->s_disp)))
315 		SYSCTL_ADD_PROC(&sc->sysctl_ctx,
316 		    SYSCTL_CHILDREN(sc->sysctl_tree), OID_AUTO,
317 		    "video_output", CTLTYPE_INT | CTLFLAG_RW, sc, 0,
318 		    acpi_asus_sysctl_disp, "I", "display output state");
319 
320 	/* Attach LCD state, easy for most models... */
321 	if (sc->model->lcd_get && strncmp(sc->model->name, "L3H", 3) != 0 &&
322 	    ACPI_SUCCESS(acpi_GetInteger(sc->handle, sc->model->lcd_get,
323 	    &sc->s_lcd)))
324 		SYSCTL_ADD_PROC(&sc->sysctl_ctx,
325 		    SYSCTL_CHILDREN(sc->sysctl_tree), OID_AUTO,
326 		    "lcd_backlight", CTLTYPE_INT | CTLFLAG_RW, sc, 0,
327 		    acpi_asus_sysctl_lcd, "I", "state of the lcd backlight");
328 
329 	/* ...a nightmare for the L3H */
330 	else if (sc->model->lcd_get) {
331 		ACPI_BUFFER		Buf;
332 		ACPI_OBJECT		Arg[2], Obj;
333 		ACPI_OBJECT_LIST	Args;
334 
335 		Arg[0].Type = ACPI_TYPE_INTEGER;
336 		Arg[0].Integer.Value = 0x02;
337 		Arg[1].Type = ACPI_TYPE_INTEGER;
338 		Arg[1].Integer.Value = 0x03;
339 
340 		Args.Count = 2;
341 		Args.Pointer = Arg;
342 
343 		Buf.Length = sizeof(Obj);
344 		Buf.Pointer = &Obj;
345 
346 		if (ACPI_SUCCESS(AcpiEvaluateObject(sc->handle,
347 		    sc->model->lcd_get, &Args, &Buf)) &&
348 		    Obj.Type == ACPI_TYPE_INTEGER) {
349 			sc->s_lcd = Obj.Integer.Value >> 8;
350 
351 			SYSCTL_ADD_PROC(&sc->sysctl_ctx,
352 			    SYSCTL_CHILDREN(sc->sysctl_tree), OID_AUTO,
353 			    "lcd_backlight", CTLTYPE_INT | CTLFLAG_RW, sc, 0,
354 			    acpi_asus_sysctl_lcd, "I",
355 			    "state of the lcd backlight");
356 		}
357 	}
358 
359 	/* Activate hotkeys */
360 	AcpiEvaluateObject(sc->handle, "BSTS", NULL, NULL);
361 
362 	/* Handle notifies */
363 	AcpiInstallNotifyHandler(sc->handle, ACPI_SYSTEM_NOTIFY,
364 	    acpi_asus_notify, dev);
365 
366 	return (0);
367 }
368 
369 static int
370 acpi_asus_detach(device_t dev)
371 {
372 	struct acpi_asus_softc	*sc;
373 
374 	ACPI_FUNCTION_TRACE((char *)(uintptr_t)__func__);
375 
376 	sc = device_get_softc(dev);
377 
378 	/* Turn the lights off */
379 	if (sc->model->mled_set)
380 		led_destroy(sc->s_mled);
381 
382 	if (sc->model->tled_set)
383 		led_destroy(sc->s_tled);
384 
385 	if (sc->model->wled_set)
386 		led_destroy(sc->s_wled);
387 
388 	/* Remove notify handler */
389 	AcpiRemoveNotifyHandler(sc->handle, ACPI_SYSTEM_NOTIFY,
390 	    acpi_asus_notify);
391 
392 	/* Free sysctl tree */
393 	sysctl_ctx_free(&sc->sysctl_ctx);
394 
395 	return (0);
396 }
397 
398 static void
399 acpi_asus_mled(device_t dev, int state)
400 {
401 	struct acpi_asus_softc	*sc;
402 	ACPI_OBJECT		Arg;
403 	ACPI_OBJECT_LIST	Args;
404 
405 	ACPI_FUNCTION_TRACE((char *)(uintptr_t)__func__);
406 
407 	sc = device_get_softc(dev);
408 
409 	Arg.Type = ACPI_TYPE_INTEGER;
410 	Arg.Integer.Value = !state;	/* Inverted, yes! */
411 
412 	Args.Count = 1;
413 	Args.Pointer = &Arg;
414 
415 	AcpiEvaluateObject(sc->handle, sc->model->mled_set, &Args, NULL);
416 }
417 
418 static void
419 acpi_asus_tled(device_t dev, int state)
420 {
421 	struct acpi_asus_softc	*sc;
422 	ACPI_OBJECT		Arg;
423 	ACPI_OBJECT_LIST	Args;
424 
425 	ACPI_FUNCTION_TRACE((char *)(uintptr_t)__func__);
426 
427 	sc = device_get_softc(dev);
428 
429 	Arg.Type = ACPI_TYPE_INTEGER;
430 	Arg.Integer.Value = state;
431 
432 	Args.Count = 1;
433 	Args.Pointer = &Arg;
434 
435 	AcpiEvaluateObject(sc->handle, sc->model->tled_set, &Args, NULL);
436 }
437 
438 static void
439 acpi_asus_wled(device_t dev, int state)
440 {
441 	struct acpi_asus_softc	*sc;
442 	ACPI_OBJECT		Arg;
443 	ACPI_OBJECT_LIST	Args;
444 
445 	ACPI_FUNCTION_TRACE((char *)(uintptr_t)__func__);
446 
447 	sc = device_get_softc(dev);
448 
449 	Arg.Type = ACPI_TYPE_INTEGER;
450 	Arg.Integer.Value = state;
451 
452 	Args.Count = 1;
453 	Args.Pointer = &Arg;
454 
455 	AcpiEvaluateObject(sc->handle, sc->model->wled_set, &Args, NULL);
456 }
457 
458 static int
459 acpi_asus_sysctl_brn(SYSCTL_HANDLER_ARGS)
460 {
461 	struct acpi_asus_softc	*sc;
462 	ACPI_OBJECT		Arg;
463 	ACPI_OBJECT_LIST	Args;
464 	int			brn, err;
465 
466 	ACPI_FUNCTION_TRACE((char *)(uintptr_t)__func__);
467 
468 	sc = (struct acpi_asus_softc *)oidp->oid_arg1;
469 
470 	/* Sanity check */
471 	brn = sc->s_brn;
472 	err = sysctl_handle_int(oidp, &brn, 0, req);
473 
474 	if (err != 0 || req->newptr == NULL)
475 		return (err);
476 
477 	if (brn < 0 || brn > 15)
478 		return (EINVAL);
479 
480 	/* Keep track and update */
481 	sc->s_brn = brn;
482 
483 	Arg.Type = ACPI_TYPE_INTEGER;
484 	Arg.Integer.Value = brn;
485 
486 	Args.Count = 1;
487 	Args.Pointer = &Arg;
488 
489 	if (sc->model->brn_set)
490 		AcpiEvaluateObject(sc->handle, sc->model->brn_set, &Args, NULL);
491 	else {
492 		brn -= sc->s_brn;
493 
494 		while (brn != 0) {
495 			AcpiEvaluateObject(sc->handle, (brn > 0) ?
496 			    sc->model->brn_up : sc->model->brn_dn,
497 			    NULL, NULL);
498 
499 			(brn > 0) ? brn-- : brn++;
500 		}
501 	}
502 
503 	return (0);
504 }
505 
506 static int
507 acpi_asus_sysctl_lcd(SYSCTL_HANDLER_ARGS)
508 {
509 	struct acpi_asus_softc	*sc;
510 	int			lcd, err;
511 
512 	ACPI_FUNCTION_TRACE((char *)(uintptr_t)__func__);
513 
514 	sc = (struct acpi_asus_softc *)oidp->oid_arg1;
515 
516 	/* Sanity check */
517 	lcd = sc->s_lcd;
518 	err = sysctl_handle_int(oidp, &lcd, 0, req);
519 
520 	if (err != 0 || req->newptr == NULL)
521 		return (err);
522 
523 	if (lcd < 0 || lcd > 1)
524 		return (EINVAL);
525 
526 	/* Keep track and update */
527 	sc->s_lcd = lcd;
528 
529 	/* Most models just need a lcd_set evaluated, the L3H is trickier */
530 	if (strncmp(sc->model->name, "L3H", 3) != 0)
531 		AcpiEvaluateObject(sc->handle, sc->model->lcd_set, NULL, NULL);
532 	else {
533 		ACPI_OBJECT		Arg;
534 		ACPI_OBJECT_LIST	Args;
535 
536 		Arg.Type = ACPI_TYPE_INTEGER;
537 		Arg.Integer.Value = 0x07;
538 
539 		Args.Count = 1;
540 		Args.Pointer = &Arg;
541 
542 		AcpiEvaluateObject(sc->handle, sc->model->lcd_set, &Args, NULL);
543 	}
544 
545 	return (0);
546 }
547 
548 static int
549 acpi_asus_sysctl_disp(SYSCTL_HANDLER_ARGS)
550 {
551 	struct acpi_asus_softc	*sc;
552 	ACPI_OBJECT		Arg;
553 	ACPI_OBJECT_LIST	Args;
554 	int			disp, err;
555 
556 	ACPI_FUNCTION_TRACE((char *)(uintptr_t)__func__);
557 
558 	sc = (struct acpi_asus_softc *)oidp->oid_arg1;
559 
560 	/* Sanity check */
561 	disp = sc->s_disp;
562 	err = sysctl_handle_int(oidp, &disp, 0, req);
563 
564 	if (err != 0 || req->newptr == NULL)
565 		return (err);
566 
567 	if (disp < 0 || disp > 7)
568 		return (EINVAL);
569 
570 	/* Keep track and update */
571 	sc->s_disp = disp;
572 
573 	Arg.Type = ACPI_TYPE_INTEGER;
574 	Arg.Integer.Value = disp;
575 
576 	Args.Count = 1;
577 	Args.Pointer = &Arg;
578 
579 	AcpiEvaluateObject(sc->handle, sc->model->disp_set, &Args, NULL);
580 
581 	return (0);
582 }
583 
584 static void
585 acpi_asus_notify(ACPI_HANDLE h, UINT32 notify, void *context)
586 {
587 	struct acpi_asus_softc	*sc;
588 	struct acpi_softc	*acpi_sc;
589 
590 	ACPI_FUNCTION_TRACE((char *)(uintptr_t)__func__);
591 
592 	sc = device_get_softc((device_t)context);
593 	acpi_sc = acpi_device_get_parent_softc(sc->dev);
594 
595 	if ((notify & ~0x10) <= 15) {
596 		sc->s_brn = notify & ~0x10;
597 		ACPI_VPRINT(sc->dev, acpi_sc, "Brightness increased\n");
598 	} else if ((notify & ~0x20) <= 15) {
599 		sc->s_brn = notify & ~0x20;
600 		ACPI_VPRINT(sc->dev, acpi_sc, "Brightness decreased\n");
601 	} else if (notify == 0x33) {
602 		sc->s_lcd = 1;
603 		ACPI_VPRINT(sc->dev, acpi_sc, "LCD turned on\n");
604 	} else if (notify == 0x34) {
605 		sc->s_lcd = 0;
606 		ACPI_VPRINT(sc->dev, acpi_sc, "LCD turned off\n");
607 	} else {
608 		/* Notify devd(8) */
609 		acpi_UserNotify("ASUS", h, notify);
610 	}
611 }
612