Add interface properties for device details
[connman] / src / iface.c
1 /*
2  *
3  *  Connection Manager
4  *
5  *  Copyright (C) 2007  Intel Corporation. All rights reserved.
6  *
7  *  This program is free software; you can redistribute it and/or modify
8  *  it under the terms of the GNU General Public License version 2 as
9  *  published by the Free Software Foundation.
10  *
11  *  This program is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU General Public License for more details.
15  *
16  *  You should have received a copy of the GNU General Public License
17  *  along with this program; if not, write to the Free Software
18  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19  *
20  */
21
22 #ifdef HAVE_CONFIG_H
23 #include <config.h>
24 #endif
25
26 #include <stdio.h>
27 #include <errno.h>
28 #include <unistd.h>
29 #include <stdlib.h>
30 #include <string.h>
31 #include <sys/ioctl.h>
32 #include <sys/socket.h>
33 #include <netinet/in.h>
34 #include <arpa/inet.h>
35 #include <net/if.h>
36 #include <net/route.h>
37
38 #include <linux/netlink.h>
39 #include <linux/rtnetlink.h>
40
41 #include <glib.h>
42 #include <gdbus.h>
43
44 #include <hal/libhal.h>
45
46 #include "connman.h"
47
48 static GSList *drivers = NULL;
49
50 int connman_iface_register(struct connman_iface_driver *driver)
51 {
52         DBG("driver %p", driver);
53
54         drivers = g_slist_append(drivers, driver);
55
56         return 0;
57 }
58
59 void connman_iface_unregister(struct connman_iface_driver *driver)
60 {
61         DBG("driver %p", driver);
62
63         drivers = g_slist_remove(drivers, driver);
64 }
65
66 static GSList *interfaces = NULL;
67
68 struct connman_iface *__connman_iface_find(int index)
69 {
70         GSList *list;
71
72         for (list = interfaces; list; list = list->next) {
73                 struct connman_iface *iface = list->data;
74
75                 if (iface->index == index)
76                         return iface;
77         }
78
79         return NULL;
80 }
81
82 void __connman_iface_list(DBusMessageIter *iter)
83 {
84         GSList *list;
85
86         DBG("");
87
88         for (list = interfaces; list; list = list->next) {
89                 struct connman_iface *iface = list->data;
90
91                 dbus_message_iter_append_basic(iter,
92                                 DBUS_TYPE_OBJECT_PATH, &iface->path);
93         }
94 }
95
96 int connman_iface_update(struct connman_iface *iface,
97                                         enum connman_iface_state state)
98 {
99         switch (state) {
100         case CONNMAN_IFACE_STATE_ACTIVE:
101                 if (iface->type == CONNMAN_IFACE_TYPE_80211) {
102                         if (iface->driver->connect)
103                                 iface->driver->connect(iface, NULL);
104                 }
105                 break;
106
107         case CONNMAN_IFACE_STATE_CONNECTED:
108                 __connman_dhcp_request(iface);
109                 break;
110
111         default:
112                 break;
113         }
114
115         iface->state = state;
116
117         return 0;
118 }
119
120 void connman_iface_indicate_carrier(struct connman_iface *iface, int carrier)
121 {
122         DBG("iface %p carrier %d", iface, carrier);
123 }
124
125 int connman_iface_get_ipv4(struct connman_iface *iface,
126                                                 struct connman_ipv4 *ipv4)
127 {
128         struct {
129                 struct nlmsghdr hdr;
130                 struct rtgenmsg msg;
131         } req;
132
133         if ((iface->flags & CONNMAN_IFACE_FLAG_RTNL) == 0)
134                 return -1;
135
136         DBG("iface %p ipv4 %p", iface, ipv4);
137
138         memset(&req, 0, sizeof(req));
139         req.hdr.nlmsg_len = sizeof(req.hdr) + sizeof(req.msg);
140         req.hdr.nlmsg_type = RTM_GETADDR;
141         req.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
142         req.hdr.nlmsg_pid = 0;
143         req.hdr.nlmsg_seq = 4711;
144         req.msg.rtgen_family = AF_INET;
145
146         __connman_rtnl_send(&req, sizeof(req));
147
148         return 0;
149 }
150
151 int connman_iface_set_ipv4(struct connman_iface *iface,
152                                                 struct connman_ipv4 *ipv4)
153 {
154         struct ifreq ifr;
155         struct rtentry rt;
156         struct sockaddr_in *addr;
157         char cmd[128];
158         int sk, err;
159
160         if ((iface->flags & CONNMAN_IFACE_FLAG_RTNL) == 0)
161                 return -1;
162
163         DBG("iface %p ipv4 %p", iface, ipv4);
164
165         sk = socket(PF_INET, SOCK_DGRAM, 0);
166         if (sk < 0)
167                 return -1;
168
169         memset(&ifr, 0, sizeof(ifr));
170         ifr.ifr_ifindex = iface->index;
171
172         if (ioctl(sk, SIOCGIFNAME, &ifr) < 0) {
173                 close(sk);
174                 return -1;
175         }
176
177         DBG("ifname %s", ifr.ifr_name);
178
179         addr = (struct sockaddr_in *) &ifr.ifr_addr;
180         addr->sin_family = AF_INET;
181         addr->sin_addr = ipv4->address;
182
183         err = ioctl(sk, SIOCSIFADDR, &ifr);
184
185         if (err < 0)
186                 DBG("address setting failed (%s)", strerror(errno));
187
188         addr = (struct sockaddr_in *) &ifr.ifr_netmask;
189         addr->sin_family = AF_INET;
190         addr->sin_addr = ipv4->netmask;
191
192         err = ioctl(sk, SIOCSIFNETMASK, &ifr);
193
194         if (err < 0)
195                 DBG("netmask setting failed (%s)", strerror(errno));
196
197         addr = (struct sockaddr_in *) &ifr.ifr_broadaddr;
198         addr->sin_family = AF_INET;
199         addr->sin_addr = ipv4->broadcast;
200
201         err = ioctl(sk, SIOCSIFBRDADDR, &ifr);
202
203         if (err < 0)
204                 DBG("broadcast setting failed (%s)", strerror(errno));
205
206         memset(&rt, 0, sizeof(rt));
207         rt.rt_flags = RTF_UP | RTF_GATEWAY;
208
209         addr = (struct sockaddr_in *) &rt.rt_dst;
210         addr->sin_family = AF_INET;
211         addr->sin_addr.s_addr = INADDR_ANY;
212
213         addr = (struct sockaddr_in *) &rt.rt_gateway;
214         addr->sin_family = AF_INET;
215         addr->sin_addr = ipv4->gateway;
216
217         addr = (struct sockaddr_in *) &rt.rt_genmask;
218         addr->sin_family = AF_INET;
219         addr->sin_addr.s_addr = INADDR_ANY;
220
221         err = ioctl(sk, SIOCADDRT, &rt);
222
223         close(sk);
224
225         if (err < 0) {
226                 DBG("default route failed (%s)", strerror(errno));
227                 return -1;
228         }
229
230         sprintf(cmd, "echo \"nameserver %s\" | resolvconf -a %s",
231                                 inet_ntoa(ipv4->nameserver), ifr.ifr_name);
232
233         DBG("%s", cmd);
234
235         system(cmd);
236
237         return 0;
238 }
239
240 int connman_iface_clear_ipv4(struct connman_iface *iface)
241 {
242         struct ifreq ifr;
243         struct sockaddr_in *addr;
244         char cmd[128];
245         int sk, err;
246
247         if ((iface->flags & CONNMAN_IFACE_FLAG_RTNL) == 0)
248                 return -1;
249
250         DBG("iface %p", iface);
251
252         sk = socket(PF_INET, SOCK_DGRAM, 0);
253         if (sk < 0)
254                 return -1;
255
256         memset(&ifr, 0, sizeof(ifr));
257         ifr.ifr_ifindex = iface->index;
258
259         if (ioctl(sk, SIOCGIFNAME, &ifr) < 0) {
260                 close(sk);
261                 return -1;
262         }
263
264         DBG("ifname %s", ifr.ifr_name);
265
266         addr = (struct sockaddr_in *) &ifr.ifr_addr;
267         addr->sin_family = AF_INET;
268         addr->sin_addr.s_addr = INADDR_ANY;
269
270         //err = ioctl(sk, SIOCDIFADDR, &ifr);
271         err = ioctl(sk, SIOCSIFADDR, &ifr);
272
273         close(sk);
274
275         if (err < 0 && errno != EADDRNOTAVAIL) {
276                 DBG("address removal failed (%s)", strerror(errno));
277                 return -1;
278         }
279
280         sprintf(cmd, "resolvconf -d %s", ifr.ifr_name);
281
282         DBG("%s", cmd);
283
284         system(cmd);
285
286         return 0;
287 }
288
289 static DBusMessage *enable_iface(DBusConnection *conn,
290                                         DBusMessage *msg, void *data)
291 {
292         struct connman_iface *iface = data;
293         struct connman_iface_driver *driver = iface->driver;
294         DBusMessage *reply;
295
296         DBG("conn %p", conn);
297
298         reply = dbus_message_new_method_return(msg);
299         if (reply == NULL)
300                 return NULL;
301
302         if (driver->activate)
303                 driver->activate(iface);
304
305         dbus_message_append_args(reply, DBUS_TYPE_INVALID);
306
307         return reply;
308 }
309
310 static DBusMessage *scan_iface(DBusConnection *conn,
311                                         DBusMessage *msg, void *data)
312 {
313         struct connman_iface *iface = data;
314         struct connman_iface_driver *driver = iface->driver;
315         DBusMessage *reply;
316
317         DBG("conn %p", conn);
318
319         reply = dbus_message_new_method_return(msg);
320         if (reply == NULL)
321                 return NULL;
322
323         if (driver->scan)
324                 driver->scan(iface);
325
326         dbus_message_append_args(reply, DBUS_TYPE_INVALID);
327
328         return reply;
329 }
330
331 static DBusMessage *set_network(DBusConnection *conn,
332                                         DBusMessage *msg, void *data)
333 {
334         struct connman_iface *iface = data;
335         struct connman_iface_driver *driver = iface->driver;
336         DBusMessage *reply;
337         const char *network;
338
339         DBG("conn %p", conn);
340
341         dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &network,
342                                                         DBUS_TYPE_INVALID);
343
344         reply = dbus_message_new_method_return(msg);
345         if (reply == NULL)
346                 return NULL;
347
348         if (driver->set_network)
349                 driver->set_network(iface, network);
350
351         dbus_message_append_args(reply, DBUS_TYPE_INVALID);
352
353         return reply;
354 }
355
356 static DBusMessage *set_passphrase(DBusConnection *conn,
357                                         DBusMessage *msg, void *data)
358 {
359         struct connman_iface *iface = data;
360         struct connman_iface_driver *driver = iface->driver;
361         DBusMessage *reply;
362         const char *passphrase;
363
364         DBG("conn %p", conn);
365
366         dbus_message_get_args(msg, NULL, DBUS_TYPE_STRING, &passphrase,
367                                                         DBUS_TYPE_INVALID);
368
369         reply = dbus_message_new_method_return(msg);
370         if (reply == NULL)
371                 return NULL;
372
373         if (driver->set_passphrase)
374                 driver->set_passphrase(iface, passphrase);
375
376         dbus_message_append_args(reply, DBUS_TYPE_INVALID);
377
378         return reply;
379 }
380
381 static GDBusMethodTable iface_methods[] = {
382         { "Enable",        "",  "", enable_iface   },
383         { "Scan",          "",  "", scan_iface     },
384         { "SetNetwork",    "s", "", set_network    },
385         { "SetPassphrase", "s", "", set_passphrase },
386         { },
387 };
388
389 static dbus_bool_t get_type(DBusConnection *conn,
390                                         DBusMessageIter *iter, void *data)
391 {
392         struct connman_iface *iface = data;
393         const char *type;
394
395         DBG("iface %p", iface);
396
397         switch (iface->type) {
398         case CONNMAN_IFACE_TYPE_80203:
399                 type = "80203";
400                 break;
401         case CONNMAN_IFACE_TYPE_80211:
402                 type = "80211";
403                 break;
404         case CONNMAN_IFACE_TYPE_WIMAX:
405                 type = "wimax";
406                 break;
407         case CONNMAN_IFACE_TYPE_BLUETOOTH:
408                 type = "bluetooth";
409                 break;
410         default:
411                 type = "unknown";
412                 break;
413         }
414
415         dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &type);
416
417         return TRUE;
418 }
419
420 static dbus_bool_t get_address(DBusConnection *conn,
421                                         DBusMessageIter *iter, void *data)
422 {
423         struct connman_iface *iface = data;
424         const char *address;
425
426         DBG("iface %p", iface);
427
428         if (!iface->driver->get_address)
429                 return FALSE;
430
431         address = iface->driver->get_address(iface);
432         if (address == NULL)
433                 return FALSE;
434
435         dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING, &address);
436
437         return TRUE;
438 }
439
440 static dbus_bool_t get_driver(DBusConnection *conn,
441                                         DBusMessageIter *iter, void *data)
442 {
443         struct connman_iface *iface = data;
444
445         DBG("iface %p", iface);
446
447         if (iface->device.driver == NULL)
448                 return FALSE;
449
450         dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING,
451                                                 &iface->device.driver);
452
453         return TRUE;
454 }
455
456 static dbus_bool_t get_vendor(DBusConnection *conn,
457                                         DBusMessageIter *iter, void *data)
458 {
459         struct connman_iface *iface = data;
460
461         DBG("iface %p", iface);
462
463         if (iface->device.vendor == NULL)
464                 return FALSE;
465
466         dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING,
467                                                 &iface->device.vendor);
468
469         return TRUE;
470 }
471
472 static dbus_bool_t get_product(DBusConnection *conn,
473                                         DBusMessageIter *iter, void *data)
474 {
475         struct connman_iface *iface = data;
476
477         DBG("iface %p", iface);
478
479         if (iface->device.product == NULL)
480                 return FALSE;
481
482         dbus_message_iter_append_basic(iter, DBUS_TYPE_STRING,
483                                                 &iface->device.product);
484
485         return TRUE;
486 }
487
488 static GDBusPropertyTable iface_properties[] = {
489         { "Type",    "s", get_type    },
490         { "Address", "s", get_address },
491         { "Driver",  "s", get_driver  },
492         { "Vendor",  "s", get_vendor  },
493         { "Product", "s", get_product },
494         { },
495 };
496
497 static void device_free(void *data)
498 {
499         struct connman_iface *iface = data;
500
501         DBG("iface %p", iface);
502
503         connman_iface_clear_ipv4(iface);
504
505         if (iface->driver && iface->driver->remove)
506                 iface->driver->remove(iface);
507
508         g_free(iface->path);
509         g_free(iface->udi);
510         g_free(iface->sysfs);
511         g_free(iface->device.driver);
512         g_free(iface->device.vendor);
513         g_free(iface->device.product);
514         g_free(iface);
515 }
516
517 static void detect_device_info(LibHalContext *ctx, struct connman_iface *iface)
518 {
519         char *parent, *subsys, *value;
520
521         parent = libhal_device_get_property_string(ctx, iface->udi,
522                                                 "info.parent", NULL);
523
524         subsys = libhal_device_get_property_string(ctx, iface->udi,
525                                                 "linux.subsystem", NULL);
526
527         value = libhal_device_get_property_string(ctx, iface->udi,
528                                                 "info.linux.driver", NULL);
529         if (value == NULL) {
530                 value = libhal_device_get_property_string(ctx, parent,
531                                                 "info.linux.driver", NULL);
532                 if (value != NULL)
533                         iface->device.driver = g_strdup(value);
534         }
535
536         if (strcmp(subsys, "net") == 0) {
537                 value = libhal_device_get_property_string(ctx, parent,
538                                                         "info.vendor", NULL);
539                 if (value != NULL)
540                         iface->device.vendor = g_strdup(value);
541
542                 value = libhal_device_get_property_string(ctx, parent,
543                                                         "info.product", NULL);
544                 if (value != NULL)
545                         iface->device.product = g_strdup(value);
546         }
547 }
548
549 static int probe_device(LibHalContext *ctx,
550                         struct connman_iface_driver *driver, const char *udi)
551 {
552         DBusConnection *conn;
553         struct connman_iface *iface;
554         char *temp, *sysfs;
555         int err;
556
557         DBG("ctx %p driver %p udi %s", ctx, driver, udi);
558
559         if (!driver->probe)
560                 return -1;
561
562         iface = g_try_new0(struct connman_iface, 1);
563         if (iface == NULL)
564                 return -1;
565
566         temp = g_path_get_basename(udi);
567         iface->path = g_strdup_printf("%s/%s", CONNMAN_IFACE_BASEPATH, temp);
568         g_free(temp);
569
570         iface->udi = g_strdup(udi);
571
572         DBG("path %s", iface->path);
573
574         sysfs = libhal_device_get_property_string(ctx, udi,
575                                                 "linux.sysfs_path", NULL);
576         if (sysfs != NULL)
577                 iface->sysfs = g_strdup(sysfs);
578
579         detect_device_info(ctx, iface);
580
581         iface->index = -1;
582
583         if (g_str_has_prefix(driver->capability, "net") == TRUE)
584                 iface->index = libhal_device_get_property_int(ctx, udi,
585                                                 "net.linux.ifindex", NULL);
586
587         iface->type = CONNMAN_IFACE_TYPE_UNKNOWN;
588         iface->flags = 0;
589         iface->state = CONNMAN_IFACE_STATE_UNKNOWN;
590
591         DBG("iface %p", iface);
592
593         err = driver->probe(iface);
594         if (err < 0) {
595                 device_free(iface);
596                 return -1;
597         }
598
599         iface->driver = driver;
600
601         conn = libhal_ctx_get_dbus_connection(ctx);
602
603         g_dbus_register_object(conn, iface->path, iface, device_free);
604
605         interfaces = g_slist_append(interfaces, iface);
606
607         if (iface->flags & CONNMAN_IFACE_FLAG_IPV4) {
608                 if (driver->get_ipv4)
609                         driver->get_ipv4(iface, &iface->ipv4);
610                 else
611                         connman_iface_get_ipv4(iface, &iface->ipv4);
612
613                 DBG("address %s", inet_ntoa(iface->ipv4.address));
614         }
615
616         g_dbus_register_interface(conn, iface->path,
617                                         CONNMAN_IFACE_INTERFACE,
618                                         iface_methods, NULL, iface_properties);
619
620         g_dbus_emit_signal(conn, CONNMAN_MANAGER_PATH,
621                                         CONNMAN_MANAGER_INTERFACE,
622                                         "InterfaceAdded",
623                                         DBUS_TYPE_OBJECT_PATH, &iface->path,
624                                         DBUS_TYPE_INVALID);
625
626         return 0;
627 }
628
629 static void device_added(LibHalContext *ctx, const char *udi)
630 {
631         GSList *list;
632
633         DBG("ctx %p udi %s", ctx, udi);
634
635         for (list = drivers; list; list = list->next) {
636                 struct connman_iface_driver *driver = list->data;
637
638                 if (driver->capability == NULL)
639                         continue;
640
641                 if (libhal_device_query_capability(ctx, udi,
642                                         driver->capability, NULL) == TRUE) {
643                         if (probe_device(ctx, driver, udi) == 0)
644                                 break;
645                 }
646         }
647 }
648
649 static void device_removed(LibHalContext *ctx, const char *udi)
650 {
651         DBusConnection *conn;
652         GSList *list;
653
654         DBG("ctx %p udi %s", ctx, udi);
655
656         conn = libhal_ctx_get_dbus_connection(ctx);
657
658         for (list = interfaces; list; list = list->next) {
659                 struct connman_iface *iface = list->data;
660
661                 if (strcmp(udi, iface->udi) == 0) {
662                         g_dbus_emit_signal(conn, CONNMAN_MANAGER_PATH,
663                                         CONNMAN_MANAGER_INTERFACE,
664                                         "InterfaceRemoved",
665                                         DBUS_TYPE_OBJECT_PATH, &iface->path,
666                                         DBUS_TYPE_INVALID);
667                         interfaces = g_slist_remove(interfaces, iface);
668                         g_dbus_unregister_interface(conn, iface->path,
669                                                 CONNMAN_IFACE_INTERFACE);
670                         g_dbus_unregister_object(conn, iface->path);
671                         break;
672                 }
673         }
674 }
675
676 static void probe_driver(LibHalContext *ctx,
677                                 struct connman_iface_driver *driver)
678 {
679         char **list;
680         int num;
681
682         DBG("ctx %p driver %p", ctx, driver);
683
684         list = libhal_find_device_by_capability(ctx,
685                                         driver->capability, &num, NULL);
686         if (list) {
687                 char **tmp = list;
688
689                 while (*tmp) {
690                         probe_device(ctx, driver, *tmp);
691                         tmp++;
692                 }
693
694                 libhal_free_string_array(list);
695         }
696 }
697
698 static void find_devices(LibHalContext *ctx)
699 {
700         GSList *list;
701
702         DBG("ctx %p", ctx);
703
704         for (list = drivers; list; list = list->next) {
705                 struct connman_iface_driver *driver = list->data;
706
707                 DBG("driver %p", driver);
708
709                 if (driver->capability == NULL)
710                         continue;
711
712                 probe_driver(ctx, driver);
713         }
714 }
715
716 static LibHalContext *hal_ctx = NULL;
717
718 static void hal_init(void *data)
719 {
720         DBusConnection *conn = data;
721
722         DBG("conn %p", conn);
723
724         if (hal_ctx != NULL)
725                 return;
726
727         hal_ctx = libhal_ctx_new();
728         if (hal_ctx == NULL)
729                 return;
730
731         if (libhal_ctx_set_dbus_connection(hal_ctx, conn) == FALSE) {
732                 libhal_ctx_free(hal_ctx);
733                 return;
734         }
735
736         if (libhal_ctx_init(hal_ctx, NULL) == FALSE) {
737                 libhal_ctx_free(hal_ctx);
738                 return ;
739         }
740
741         libhal_ctx_set_device_added(hal_ctx, device_added);
742         libhal_ctx_set_device_removed(hal_ctx, device_removed);
743
744         //libhal_ctx_set_device_new_capability(hal_ctx, new_capability);
745         //libhal_ctx_set_device_lost_capability(hal_ctx, lost_capability);
746
747         find_devices(hal_ctx);
748 }
749
750 static void hal_cleanup(void *data)
751 {
752         DBusConnection *conn = data;
753         GSList *list;
754
755         DBG("conn %p", conn);
756
757         if (hal_ctx == NULL)
758                 return;
759
760         for (list = interfaces; list; list = list->next) {
761                 struct connman_iface *iface = list->data;
762
763                 DBG("path %s", iface->path);
764
765                 g_dbus_emit_signal(conn, CONNMAN_MANAGER_PATH,
766                                         CONNMAN_MANAGER_INTERFACE,
767                                         "InterfaceRemoved",
768                                         DBUS_TYPE_OBJECT_PATH, &iface->path,
769                                         DBUS_TYPE_INVALID);
770
771                 g_dbus_unregister_interface(conn, iface->path,
772                                                 CONNMAN_IFACE_INTERFACE);
773
774                 g_dbus_unregister_object(conn, iface->path);
775         }
776
777         g_slist_free(interfaces);
778
779         interfaces = NULL;
780
781         libhal_ctx_shutdown(hal_ctx, NULL);
782
783         libhal_ctx_free(hal_ctx);
784
785         hal_ctx = NULL;
786 }
787
788 static DBusConnection *connection = NULL;
789 static guint hal_watch = 0;
790
791 int __connman_iface_init(DBusConnection *conn)
792 {
793         DBG("conn %p", conn);
794
795         connection = dbus_connection_ref(conn);
796         if (connection == NULL)
797                 return -1;
798
799         hal_init(connection);
800
801         hal_watch = g_dbus_add_watch(connection, "org.freedesktop.Hal",
802                                 hal_init, hal_cleanup, connection, NULL);
803
804         return 0;
805 }
806
807 void __connman_iface_cleanup(void)
808 {
809         DBG("conn %p", connection);
810
811         g_dbus_remove_watch(connection, hal_watch);
812
813         hal_cleanup(connection);
814
815         dbus_connection_unref(connection);
816 }