diff options
Diffstat (limited to 'keyboard/hhkb/rn42/rn42_task.c')
| -rw-r--r-- | keyboard/hhkb/rn42/rn42_task.c | 478 |
1 files changed, 478 insertions, 0 deletions
diff --git a/keyboard/hhkb/rn42/rn42_task.c b/keyboard/hhkb/rn42/rn42_task.c new file mode 100644 index 000000000..5107fef48 --- /dev/null +++ b/keyboard/hhkb/rn42/rn42_task.c | |||
| @@ -0,0 +1,478 @@ | |||
| 1 | #include <stdint.h> | ||
| 2 | #include <string.h> | ||
| 3 | #include <avr/pgmspace.h> | ||
| 4 | #include <avr/eeprom.h> | ||
| 5 | #include "keycode.h" | ||
| 6 | #include "serial.h" | ||
| 7 | #include "host.h" | ||
| 8 | #include "action.h" | ||
| 9 | #include "action_util.h" | ||
| 10 | #include "lufa.h" | ||
| 11 | #include "rn42_task.h" | ||
| 12 | #include "print.h" | ||
| 13 | #include "debug.h" | ||
| 14 | #include "timer.h" | ||
| 15 | #include "wait.h" | ||
| 16 | #include "command.h" | ||
| 17 | #include "battery.h" | ||
| 18 | |||
| 19 | static bool config_mode = false; | ||
| 20 | static bool force_usb = false; | ||
| 21 | |||
| 22 | static void status_led(bool on) | ||
| 23 | { | ||
| 24 | if (on) { | ||
| 25 | DDRE |= (1<<6); | ||
| 26 | PORTE &= ~(1<<6); | ||
| 27 | } else { | ||
| 28 | DDRE |= (1<<6); | ||
| 29 | PORTE |= (1<<6); | ||
| 30 | } | ||
| 31 | } | ||
| 32 | |||
| 33 | void rn42_task_init(void) | ||
| 34 | { | ||
| 35 | battery_init(); | ||
| 36 | } | ||
| 37 | |||
| 38 | void rn42_task(void) | ||
| 39 | { | ||
| 40 | int16_t c; | ||
| 41 | // Raw mode: interpret output report of LED state | ||
| 42 | while ((c = rn42_getc()) != -1) { | ||
| 43 | // LED Out report: 0xFE, 0x02, 0x01, <leds> | ||
| 44 | // To get the report over UART set bit3 with SH, command. | ||
| 45 | static enum {LED_INIT, LED_FE, LED_02, LED_01} state = LED_INIT; | ||
| 46 | switch (state) { | ||
| 47 | case LED_INIT: | ||
| 48 | if (c == 0xFE) state = LED_FE; | ||
| 49 | else { | ||
| 50 | if (0x0 <= c && c <= 0x7f) xprintf("%c", c); | ||
| 51 | else xprintf(" %02X", c); | ||
| 52 | } | ||
| 53 | break; | ||
| 54 | case LED_FE: | ||
| 55 | if (c == 0x02) state = LED_02; | ||
| 56 | else state = LED_INIT; | ||
| 57 | break; | ||
| 58 | case LED_02: | ||
| 59 | if (c == 0x01) state = LED_01; | ||
| 60 | else state = LED_INIT; | ||
| 61 | break; | ||
| 62 | case LED_01: | ||
| 63 | dprintf("LED status: %02X\n", c); | ||
| 64 | rn42_set_leds(c); | ||
| 65 | state = LED_INIT; | ||
| 66 | break; | ||
| 67 | default: | ||
| 68 | state = LED_INIT; | ||
| 69 | } | ||
| 70 | } | ||
| 71 | |||
| 72 | /* Bluetooth mode when ready */ | ||
| 73 | if (!config_mode && !force_usb) { | ||
| 74 | if (!rn42_rts() && host_get_driver() != &rn42_driver) { | ||
| 75 | clear_keyboard(); | ||
| 76 | host_set_driver(&rn42_driver); | ||
| 77 | } else if (rn42_rts() && host_get_driver() != &lufa_driver) { | ||
| 78 | clear_keyboard(); | ||
| 79 | host_set_driver(&lufa_driver); | ||
| 80 | } | ||
| 81 | } | ||
| 82 | |||
| 83 | |||
| 84 | static uint16_t prev_timer = 0; | ||
| 85 | uint16_t e = timer_elapsed(prev_timer); | ||
| 86 | if (e > 1000) { | ||
| 87 | /* every second */ | ||
| 88 | prev_timer += e/1000*1000; | ||
| 89 | |||
| 90 | /* Low voltage alert */ | ||
| 91 | uint8_t bs = battery_status(); | ||
| 92 | if (bs == LOW_VOLTAGE) { | ||
| 93 | battery_led(LED_ON); | ||
| 94 | } else { | ||
| 95 | battery_led(LED_CHARGER); | ||
| 96 | } | ||
| 97 | |||
| 98 | /* every minute */ | ||
| 99 | uint32_t t = timer_read32()/1000; | ||
| 100 | if (t%60 == 0) { | ||
| 101 | uint16_t v = battery_voltage(); | ||
| 102 | uint8_t h = t/3600; | ||
| 103 | uint8_t m = t%3600/60; | ||
| 104 | uint8_t s = t%60; | ||
| 105 | dprintf("%02u:%02u:%02u\t%umV\n", h, m, s, v); | ||
| 106 | /* TODO: xprintf doesn't work for this. | ||
| 107 | xprintf("%02u:%02u:%02u\t%umV\n", (t/3600), (t%3600/60), (t%60), v); | ||
| 108 | */ | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | |||
| 113 | /* Connection monitor */ | ||
| 114 | if (!rn42_rts() && rn42_linked()) { | ||
| 115 | status_led(true); | ||
| 116 | } else { | ||
| 117 | status_led(false); | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | |||
| 122 | |||
| 123 | /****************************************************************************** | ||
| 124 | * Command | ||
| 125 | ******************************************************************************/ | ||
| 126 | static host_driver_t *prev_driver = &rn42_driver; | ||
| 127 | |||
| 128 | static void print_rn42(void) | ||
| 129 | { | ||
| 130 | int16_t c; | ||
| 131 | while ((c = rn42_getc()) != -1) { | ||
| 132 | xprintf("%c", c); | ||
| 133 | } | ||
| 134 | } | ||
| 135 | |||
| 136 | static void clear_rn42(void) | ||
| 137 | { | ||
| 138 | while (rn42_getc() != -1) ; | ||
| 139 | } | ||
| 140 | |||
| 141 | #define SEND_STR(str) send_str(PSTR(str)) | ||
| 142 | #define SEND_COMMAND(cmd) send_command(PSTR(cmd)) | ||
| 143 | |||
| 144 | static void send_str(const char *str) | ||
| 145 | { | ||
| 146 | uint8_t c; | ||
| 147 | while ((c = pgm_read_byte(str++))) | ||
| 148 | rn42_putc(c); | ||
| 149 | } | ||
| 150 | |||
| 151 | static const char *send_command(const char *cmd) | ||
| 152 | { | ||
| 153 | static const char *s; | ||
| 154 | send_str(cmd); | ||
| 155 | wait_ms(500); | ||
| 156 | s = rn42_gets(100); | ||
| 157 | xprintf("%s\r\n", s); | ||
| 158 | print_rn42(); | ||
| 159 | return s; | ||
| 160 | } | ||
| 161 | |||
| 162 | static void enter_command_mode(void) | ||
| 163 | { | ||
| 164 | prev_driver = host_get_driver(); | ||
| 165 | clear_keyboard(); | ||
| 166 | host_set_driver(&rn42_config_driver); // null driver; not to send a key to host | ||
| 167 | rn42_disconnect(); | ||
| 168 | while (rn42_linked()) ; | ||
| 169 | |||
| 170 | print("Entering config mode ...\n"); | ||
| 171 | wait_ms(1100); // need 1 sec | ||
| 172 | SEND_COMMAND("$$$"); | ||
| 173 | wait_ms(600); // need 1 sec | ||
| 174 | print_rn42(); | ||
| 175 | const char *s = SEND_COMMAND("v\r\n"); | ||
| 176 | if (strncmp("v", s, 1) != 0) SEND_COMMAND("+\r\n"); // local echo on | ||
| 177 | } | ||
| 178 | |||
| 179 | static void exit_command_mode(void) | ||
| 180 | { | ||
| 181 | print("Exiting config mode ...\n"); | ||
| 182 | SEND_COMMAND("---\r\n"); // exit | ||
| 183 | |||
| 184 | rn42_autoconnect(); | ||
| 185 | clear_keyboard(); | ||
| 186 | host_set_driver(prev_driver); | ||
| 187 | } | ||
| 188 | |||
| 189 | static void init_rn42(void) | ||
| 190 | { | ||
| 191 | // RN-42 configure | ||
| 192 | if (!config_mode) enter_command_mode(); | ||
| 193 | SEND_COMMAND("SF,1\r\n"); // factory defaults | ||
| 194 | SEND_COMMAND("S-,TmkBT\r\n"); | ||
| 195 | SEND_COMMAND("SS,Keyboard/Mouse\r\n"); | ||
| 196 | SEND_COMMAND("SM,4\r\n"); // auto connect(DTR) | ||
| 197 | SEND_COMMAND("SW,8000\r\n"); // Sniff disable | ||
| 198 | SEND_COMMAND("S~,6\r\n"); // HID profile | ||
| 199 | SEND_COMMAND("SH,003C\r\n"); // combo device, out-report, 4-reconnect | ||
| 200 | SEND_COMMAND("SY,FFF4\r\n"); // transmit power -12 | ||
| 201 | SEND_COMMAND("R,1\r\n"); | ||
| 202 | if (!config_mode) exit_command_mode(); | ||
| 203 | } | ||
| 204 | |||
| 205 | #if 0 | ||
| 206 | // Switching connections | ||
| 207 | // NOTE: Remote Address doesn't work in the way manual says. | ||
| 208 | // EEPROM address for link store | ||
| 209 | #define RN42_LINK0 (uint8_t *)128 | ||
| 210 | #define RN42_LINK1 (uint8_t *)140 | ||
| 211 | #define RN42_LINK2 (uint8_t *)152 | ||
| 212 | #define RN42_LINK3 (uint8_t *)164 | ||
| 213 | static void store_link(uint8_t *eeaddr) | ||
| 214 | { | ||
| 215 | enter_command_mode(); | ||
| 216 | SEND_STR("GR\r\n"); // remote address | ||
| 217 | const char *s = rn42_gets(500); | ||
| 218 | if (strcmp("GR", s) == 0) s = rn42_gets(500); // ignore local echo | ||
| 219 | xprintf("%s(%d)\r\n", s, strlen(s)); | ||
| 220 | if (strlen(s) == 12) { | ||
| 221 | for (int i = 0; i < 12; i++) { | ||
| 222 | eeprom_write_byte(eeaddr+i, *(s+i)); | ||
| 223 | dprintf("%c ", *(s+i)); | ||
| 224 | } | ||
| 225 | dprint("\r\n"); | ||
| 226 | } | ||
| 227 | exit_command_mode(); | ||
| 228 | } | ||
| 229 | |||
| 230 | static void restore_link(const uint8_t *eeaddr) | ||
| 231 | { | ||
| 232 | enter_command_mode(); | ||
| 233 | SEND_COMMAND("SR,Z\r\n"); // remove remote address | ||
| 234 | SEND_STR("SR,"); // set remote address from EEPROM | ||
| 235 | for (int i = 0; i < 12; i++) { | ||
| 236 | uint8_t c = eeprom_read_byte(eeaddr+i); | ||
| 237 | rn42_putc(c); | ||
| 238 | dprintf("%c ", c); | ||
| 239 | } | ||
| 240 | dprintf("\r\n"); | ||
| 241 | SEND_COMMAND("\r\n"); | ||
| 242 | SEND_COMMAND("R,1\r\n"); // reboot | ||
| 243 | exit_command_mode(); | ||
| 244 | } | ||
| 245 | |||
| 246 | static const char *get_link(uint8_t * eeaddr) | ||
| 247 | { | ||
| 248 | static char s[13]; | ||
| 249 | for (int i = 0; i < 12; i++) { | ||
| 250 | uint8_t c = eeprom_read_byte(eeaddr+i); | ||
| 251 | s[i] = c; | ||
| 252 | } | ||
| 253 | s[12] = '\0'; | ||
| 254 | return s; | ||
| 255 | } | ||
| 256 | #endif | ||
| 257 | |||
| 258 | static void pairing(void) | ||
| 259 | { | ||
| 260 | enter_command_mode(); | ||
| 261 | SEND_COMMAND("SR,Z\r\n"); // remove remote address | ||
| 262 | SEND_COMMAND("R,1\r\n"); // reboot | ||
| 263 | exit_command_mode(); | ||
| 264 | } | ||
| 265 | |||
| 266 | bool command_extra(uint8_t code) | ||
| 267 | { | ||
| 268 | uint32_t t; | ||
| 269 | uint16_t b; | ||
| 270 | switch (code) { | ||
| 271 | case KC_H: | ||
| 272 | case KC_SLASH: /* ? */ | ||
| 273 | print("\n\n----- Bluetooth RN-42 Help -----\n"); | ||
| 274 | print("i: RN-42 info\n"); | ||
| 275 | print("b: battery voltage\n"); | ||
| 276 | print("Del: enter/exit RN-42 config mode\n"); | ||
| 277 | print("Slck: RN-42 initialize\n"); | ||
| 278 | #if 0 | ||
| 279 | print("1-4: restore link\n"); | ||
| 280 | print("F1-F4: store link\n"); | ||
| 281 | #endif | ||
| 282 | print("p: pairing\n"); | ||
| 283 | |||
| 284 | if (config_mode) { | ||
| 285 | return true; | ||
| 286 | } else { | ||
| 287 | print("u: toggle Force USB mode\n"); | ||
| 288 | return false; // to display default command help | ||
| 289 | } | ||
| 290 | case KC_P: | ||
| 291 | pairing(); | ||
| 292 | return true; | ||
| 293 | #if 0 | ||
| 294 | /* Store link address to EEPROM */ | ||
| 295 | case KC_F1: | ||
| 296 | store_link(RN42_LINK0); | ||
| 297 | return true; | ||
| 298 | case KC_F2: | ||
| 299 | store_link(RN42_LINK1); | ||
| 300 | return true; | ||
| 301 | case KC_F3: | ||
| 302 | store_link(RN42_LINK2); | ||
| 303 | return true; | ||
| 304 | case KC_F4: | ||
| 305 | store_link(RN42_LINK3); | ||
| 306 | return true; | ||
| 307 | /* Restore link address to EEPROM */ | ||
| 308 | case KC_1: | ||
| 309 | restore_link(RN42_LINK0); | ||
| 310 | return true; | ||
| 311 | case KC_2: | ||
| 312 | restore_link(RN42_LINK1); | ||
| 313 | return true; | ||
| 314 | case KC_3: | ||
| 315 | restore_link(RN42_LINK2); | ||
| 316 | return true; | ||
| 317 | case KC_4: | ||
| 318 | restore_link(RN42_LINK3); | ||
| 319 | return true; | ||
| 320 | #endif | ||
| 321 | case KC_I: | ||
| 322 | print("\n----- RN-42 info -----\n"); | ||
| 323 | xprintf("protocol: %s\n", (host_get_driver() == &rn42_driver) ? "RN-42" : "LUFA"); | ||
| 324 | xprintf("force_usb: %X\n", force_usb); | ||
| 325 | xprintf("rn42: %s\n", rn42_rts() ? "OFF" : (rn42_linked() ? "CONN" : "ON")); | ||
| 326 | xprintf("rn42_autoconnecting(): %X\n", rn42_autoconnecting()); | ||
| 327 | xprintf("config_mode: %X\n", config_mode); | ||
| 328 | xprintf("USB State: %s\n", | ||
| 329 | (USB_DeviceState == DEVICE_STATE_Unattached) ? "Unattached" : | ||
| 330 | (USB_DeviceState == DEVICE_STATE_Powered) ? "Powered" : | ||
| 331 | (USB_DeviceState == DEVICE_STATE_Default) ? "Default" : | ||
| 332 | (USB_DeviceState == DEVICE_STATE_Addressed) ? "Addressed" : | ||
| 333 | (USB_DeviceState == DEVICE_STATE_Configured) ? "Configured" : | ||
| 334 | (USB_DeviceState == DEVICE_STATE_Suspended) ? "Suspended" : "?"); | ||
| 335 | xprintf("battery: "); | ||
| 336 | switch (battery_status()) { | ||
| 337 | case FULL_CHARGED: xprintf("FULL"); break; | ||
| 338 | case CHARGING: xprintf("CHARG"); break; | ||
| 339 | case DISCHARGING: xprintf("DISCHG"); break; | ||
| 340 | case LOW_VOLTAGE: xprintf("LOW"); break; | ||
| 341 | default: xprintf("?"); break; | ||
| 342 | }; | ||
| 343 | xprintf("\n"); | ||
| 344 | xprintf("RemoteWakeupEnabled: %X\n", USB_Device_RemoteWakeupEnabled); | ||
| 345 | xprintf("VBUS: %X\n", USBSTA&(1<<VBUS)); | ||
| 346 | t = timer_read32()/1000; | ||
| 347 | uint8_t d = t/3600/24; | ||
| 348 | uint8_t h = t/3600; | ||
| 349 | uint8_t m = t%3600/60; | ||
| 350 | uint8_t s = t%60; | ||
| 351 | xprintf("uptime: %02u %02u:%02u:%02u\n", d, h, m, s); | ||
| 352 | #if 0 | ||
| 353 | xprintf("LINK0: %s\r\n", get_link(RN42_LINK0)); | ||
| 354 | xprintf("LINK1: %s\r\n", get_link(RN42_LINK1)); | ||
| 355 | xprintf("LINK2: %s\r\n", get_link(RN42_LINK2)); | ||
| 356 | xprintf("LINK3: %s\r\n", get_link(RN42_LINK3)); | ||
| 357 | #endif | ||
| 358 | return true; | ||
| 359 | case KC_B: | ||
| 360 | // battery monitor | ||
| 361 | t = timer_read32()/1000; | ||
| 362 | b = battery_voltage(); | ||
| 363 | xprintf("BAT: %umV\t", b); | ||
| 364 | xprintf("%02u:", t/3600); | ||
| 365 | xprintf("%02u:", t%3600/60); | ||
| 366 | xprintf("%02u\n", t%60); | ||
| 367 | return true; | ||
| 368 | case KC_U: | ||
| 369 | if (config_mode) return false; | ||
| 370 | if (force_usb) { | ||
| 371 | print("Auto mode\n"); | ||
| 372 | force_usb = false; | ||
| 373 | } else { | ||
| 374 | print("USB mode\n"); | ||
| 375 | force_usb = true; | ||
| 376 | clear_keyboard(); | ||
| 377 | host_set_driver(&lufa_driver); | ||
| 378 | } | ||
| 379 | return true; | ||
| 380 | case KC_DELETE: | ||
| 381 | /* RN-42 Command mode */ | ||
| 382 | if (rn42_autoconnecting()) { | ||
| 383 | enter_command_mode(); | ||
| 384 | |||
| 385 | command_state = CONSOLE; | ||
| 386 | config_mode = true; | ||
| 387 | } else { | ||
| 388 | exit_command_mode(); | ||
| 389 | |||
| 390 | command_state = ONESHOT; | ||
| 391 | config_mode = false; | ||
| 392 | } | ||
| 393 | return true; | ||
| 394 | case KC_SCROLLLOCK: | ||
| 395 | init_rn42(); | ||
| 396 | return true; | ||
| 397 | default: | ||
| 398 | if (config_mode) | ||
| 399 | return true; | ||
| 400 | else | ||
| 401 | return false; // yield to default command | ||
| 402 | } | ||
| 403 | return true; | ||
| 404 | } | ||
| 405 | |||
| 406 | /* | ||
| 407 | * RN-42 Command mode | ||
| 408 | * sends charactors to the module | ||
| 409 | */ | ||
| 410 | static uint8_t code2asc(uint8_t code); | ||
| 411 | bool command_console_extra(uint8_t code) | ||
| 412 | { | ||
| 413 | rn42_putc(code2asc(code)); | ||
| 414 | return true; | ||
| 415 | } | ||
| 416 | |||
| 417 | // convert keycode into ascii charactor | ||
| 418 | static uint8_t code2asc(uint8_t code) | ||
| 419 | { | ||
| 420 | bool shifted = (get_mods() & (MOD_BIT(KC_LSHIFT)|MOD_BIT(KC_RSHIFT))) ? true : false; | ||
| 421 | switch (code) { | ||
| 422 | case KC_A: return (shifted ? 'A' : 'a'); | ||
| 423 | case KC_B: return (shifted ? 'B' : 'b'); | ||
| 424 | case KC_C: return (shifted ? 'C' : 'c'); | ||
| 425 | case KC_D: return (shifted ? 'D' : 'd'); | ||
| 426 | case KC_E: return (shifted ? 'E' : 'e'); | ||
| 427 | case KC_F: return (shifted ? 'F' : 'f'); | ||
| 428 | case KC_G: return (shifted ? 'G' : 'g'); | ||
| 429 | case KC_H: return (shifted ? 'H' : 'h'); | ||
| 430 | case KC_I: return (shifted ? 'I' : 'i'); | ||
| 431 | case KC_J: return (shifted ? 'J' : 'j'); | ||
| 432 | case KC_K: return (shifted ? 'K' : 'k'); | ||
| 433 | case KC_L: return (shifted ? 'L' : 'l'); | ||
| 434 | case KC_M: return (shifted ? 'M' : 'm'); | ||
| 435 | case KC_N: return (shifted ? 'N' : 'n'); | ||
| 436 | case KC_O: return (shifted ? 'O' : 'o'); | ||
| 437 | case KC_P: return (shifted ? 'P' : 'p'); | ||
| 438 | case KC_Q: return (shifted ? 'Q' : 'q'); | ||
| 439 | case KC_R: return (shifted ? 'R' : 'r'); | ||
| 440 | case KC_S: return (shifted ? 'S' : 's'); | ||
| 441 | case KC_T: return (shifted ? 'T' : 't'); | ||
| 442 | case KC_U: return (shifted ? 'U' : 'u'); | ||
| 443 | case KC_V: return (shifted ? 'V' : 'v'); | ||
| 444 | case KC_W: return (shifted ? 'W' : 'w'); | ||
| 445 | case KC_X: return (shifted ? 'X' : 'x'); | ||
| 446 | case KC_Y: return (shifted ? 'Y' : 'y'); | ||
| 447 | case KC_Z: return (shifted ? 'Z' : 'z'); | ||
| 448 | case KC_1: return (shifted ? '!' : '1'); | ||
| 449 | case KC_2: return (shifted ? '@' : '2'); | ||
| 450 | case KC_3: return (shifted ? '#' : '3'); | ||
| 451 | case KC_4: return (shifted ? '$' : '4'); | ||
| 452 | case KC_5: return (shifted ? '%' : '5'); | ||
| 453 | case KC_6: return (shifted ? '^' : '6'); | ||
| 454 | case KC_7: return (shifted ? '&' : '7'); | ||
| 455 | case KC_8: return (shifted ? '*' : '8'); | ||
| 456 | case KC_9: return (shifted ? '(' : '9'); | ||
| 457 | case KC_0: return (shifted ? ')' : '0'); | ||
| 458 | case KC_ENTER: return '\n'; | ||
| 459 | case KC_ESCAPE: return 0x1B; | ||
| 460 | case KC_BSPACE: return '\b'; | ||
| 461 | case KC_TAB: return '\t'; | ||
| 462 | case KC_SPACE: return ' '; | ||
| 463 | case KC_MINUS: return (shifted ? '_' : '-'); | ||
| 464 | case KC_EQUAL: return (shifted ? '+' : '='); | ||
| 465 | case KC_LBRACKET: return (shifted ? '{' : '['); | ||
| 466 | case KC_RBRACKET: return (shifted ? '}' : ']'); | ||
| 467 | case KC_BSLASH: return (shifted ? '|' : '\\'); | ||
| 468 | case KC_NONUS_HASH: return (shifted ? '|' : '\\'); | ||
| 469 | case KC_SCOLON: return (shifted ? ':' : ';'); | ||
| 470 | case KC_QUOTE: return (shifted ? '"' : '\''); | ||
| 471 | case KC_GRAVE: return (shifted ? '~' : '`'); | ||
| 472 | case KC_COMMA: return (shifted ? '<' : ','); | ||
| 473 | case KC_DOT: return (shifted ? '>' : '.'); | ||
| 474 | case KC_SLASH: return (shifted ? '?' : '/'); | ||
| 475 | case KC_DELETE: return '\0'; // Delete to disconnect | ||
| 476 | default: return ' '; | ||
| 477 | } | ||
| 478 | } | ||
