Dependencies:   microbit

Committer:
xx316
Date:
Wed Jun 12 15:21:01 2019 +0000
Revision:
1:032b96b05e51
for demo on Thursday;

Who changed what in which revision?

UserRevisionLine numberNew contents of line
xx316 1:032b96b05e51 1 /* mbed Microcontroller Library
xx316 1:032b96b05e51 2 * Copyright (c) 2015 ARM Limited
xx316 1:032b96b05e51 3 *
xx316 1:032b96b05e51 4 * Licensed under the Apache License, Version 2.0 (the "License");
xx316 1:032b96b05e51 5 * you may not use this file except in compliance with the License.
xx316 1:032b96b05e51 6 * You may obtain a copy of the License at
xx316 1:032b96b05e51 7 *
xx316 1:032b96b05e51 8 * http://www.apache.org/licenses/LICENSE-2.0
xx316 1:032b96b05e51 9 *
xx316 1:032b96b05e51 10 * Unless required by applicable law or agreed to in writing, software
xx316 1:032b96b05e51 11 * distributed under the License is distributed on an "AS IS" BASIS,
xx316 1:032b96b05e51 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
xx316 1:032b96b05e51 13 * See the License for the specific language governing permissions and
xx316 1:032b96b05e51 14 * limitations under the License.
xx316 1:032b96b05e51 15 */
xx316 1:032b96b05e51 16
xx316 1:032b96b05e51 17 #include <errno.h>
xx316 1:032b96b05e51 18 #include "mbed.h"
xx316 1:032b96b05e51 19 #include "CircularBuffer.h"
xx316 1:032b96b05e51 20
xx316 1:032b96b05e51 21 #include "HIDServiceBase.h"
xx316 1:032b96b05e51 22 #include "Keyboard_types.h"
xx316 1:032b96b05e51 23
xx316 1:032b96b05e51 24 /* TODO: make this easier to configure by application (e.g. as a template parameter for
xx316 1:032b96b05e51 25 * KeyboardService) */
xx316 1:032b96b05e51 26 #ifndef KEYBUFFER_SIZE
xx316 1:032b96b05e51 27 #define KEYBUFFER_SIZE 512
xx316 1:032b96b05e51 28 #endif
xx316 1:032b96b05e51 29
xx316 1:032b96b05e51 30 /**
xx316 1:032b96b05e51 31 * Report descriptor for a standard 101 keys keyboard, following the HID specification example:
xx316 1:032b96b05e51 32 * - 8 bytes input report (1 byte for modifiers and 6 for keys)
xx316 1:032b96b05e51 33 * - 1 byte output report (LEDs)
xx316 1:032b96b05e51 34 */
xx316 1:032b96b05e51 35 report_map_t KEYBOARD_REPORT_MAP = {
xx316 1:032b96b05e51 36 USAGE_PAGE(1), 0x01, // Generic Desktop Ctrls
xx316 1:032b96b05e51 37 USAGE(1), 0x06, // Keyboard
xx316 1:032b96b05e51 38 COLLECTION(1), 0x01, // Application
xx316 1:032b96b05e51 39 USAGE_PAGE(1), 0x07, // Kbrd/Keypad
xx316 1:032b96b05e51 40 USAGE_MINIMUM(1), 0xE0,
xx316 1:032b96b05e51 41 USAGE_MAXIMUM(1), 0xE7,
xx316 1:032b96b05e51 42 LOGICAL_MINIMUM(1), 0x00,
xx316 1:032b96b05e51 43 LOGICAL_MAXIMUM(1), 0x01,
xx316 1:032b96b05e51 44 REPORT_SIZE(1), 0x01, // 1 byte (Modifier)
xx316 1:032b96b05e51 45 REPORT_COUNT(1), 0x08,
xx316 1:032b96b05e51 46 INPUT(1), 0x02, // Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position
xx316 1:032b96b05e51 47 REPORT_COUNT(1), 0x01, // 1 byte (Reserved)
xx316 1:032b96b05e51 48 REPORT_SIZE(1), 0x08,
xx316 1:032b96b05e51 49 INPUT(1), 0x01, // Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position
xx316 1:032b96b05e51 50 REPORT_COUNT(1), 0x05, // 5 bits (Num lock, Caps lock, Scroll lock, Compose, Kana)
xx316 1:032b96b05e51 51 REPORT_SIZE(1), 0x01,
xx316 1:032b96b05e51 52 USAGE_PAGE(1), 0x08, // LEDs
xx316 1:032b96b05e51 53 USAGE_MINIMUM(1), 0x01, // Num Lock
xx316 1:032b96b05e51 54 USAGE_MAXIMUM(1), 0x05, // Kana
xx316 1:032b96b05e51 55 OUTPUT(1), 0x02, // Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile
xx316 1:032b96b05e51 56 REPORT_COUNT(1), 0x01, // 3 bits (Padding)
xx316 1:032b96b05e51 57 REPORT_SIZE(1), 0x03,
xx316 1:032b96b05e51 58 OUTPUT(1), 0x01, // Const,Array,Abs,No Wrap,Linear,Preferred State,No Null Position,Non-volatile
xx316 1:032b96b05e51 59 REPORT_COUNT(1), 0x06, // 6 bytes (Keys)
xx316 1:032b96b05e51 60 REPORT_SIZE(1), 0x08,
xx316 1:032b96b05e51 61 LOGICAL_MINIMUM(1), 0x00,
xx316 1:032b96b05e51 62 LOGICAL_MAXIMUM(1), 0x65, // 101 keys
xx316 1:032b96b05e51 63 USAGE_PAGE(1), 0x07, // Kbrd/Keypad
xx316 1:032b96b05e51 64 USAGE_MINIMUM(1), 0x00,
xx316 1:032b96b05e51 65 USAGE_MAXIMUM(1), 0x65,
xx316 1:032b96b05e51 66 INPUT(1), 0x00, // Data,Array,Abs,No Wrap,Linear,Preferred State,No Null Position
xx316 1:032b96b05e51 67 END_COLLECTION(0),
xx316 1:032b96b05e51 68 };
xx316 1:032b96b05e51 69
xx316 1:032b96b05e51 70 /// "keys pressed" report
xx316 1:032b96b05e51 71 static uint8_t inputReportData[] = { 0, 0, 0, 0, 0, 0, 0, 0 };
xx316 1:032b96b05e51 72 /// "keys released" report
xx316 1:032b96b05e51 73 static const uint8_t emptyInputReportData[] = { 0, 0, 0, 0, 0, 0, 0, 0 };
xx316 1:032b96b05e51 74 /// LEDs report
xx316 1:032b96b05e51 75 static uint8_t outputReportData[] = { 0 };
xx316 1:032b96b05e51 76
xx316 1:032b96b05e51 77
xx316 1:032b96b05e51 78 /**
xx316 1:032b96b05e51 79 * @class KeyBuffer
xx316 1:032b96b05e51 80 *
xx316 1:032b96b05e51 81 * Buffer used to store keys to send.
xx316 1:032b96b05e51 82 * Internally, it is a CircularBuffer, with the added capability of putting the last char back in,
xx316 1:032b96b05e51 83 * when we're unable to send it (ie. when BLE stack is busy)
xx316 1:032b96b05e51 84 */
xx316 1:032b96b05e51 85 class KeyBuffer: public CircularBuffer<uint8_t, KEYBUFFER_SIZE>
xx316 1:032b96b05e51 86 {
xx316 1:032b96b05e51 87 public:
xx316 1:032b96b05e51 88 KeyBuffer() :
xx316 1:032b96b05e51 89 CircularBuffer(),
xx316 1:032b96b05e51 90 dataIsPending (false),
xx316 1:032b96b05e51 91 keyUpIsPending (false)
xx316 1:032b96b05e51 92 {
xx316 1:032b96b05e51 93 }
xx316 1:032b96b05e51 94
xx316 1:032b96b05e51 95 /**
xx316 1:032b96b05e51 96 * Mark a character as pending. When a freshly popped character cannot be sent, because the
xx316 1:032b96b05e51 97 * underlying stack is busy, we set it as pending, and it will get popped in priority by @ref
xx316 1:032b96b05e51 98 * getPending once reports can be sent again.
xx316 1:032b96b05e51 99 *
xx316 1:032b96b05e51 100 * @param data The character to send in priority. The second keyUp report is implied.
xx316 1:032b96b05e51 101 */
xx316 1:032b96b05e51 102 void setPending(uint8_t data)
xx316 1:032b96b05e51 103 {
xx316 1:032b96b05e51 104 MBED_ASSERT(dataIsPending == false);
xx316 1:032b96b05e51 105
xx316 1:032b96b05e51 106 dataIsPending = true;
xx316 1:032b96b05e51 107 pendingData = data;
xx316 1:032b96b05e51 108 keyUpIsPending = true;
xx316 1:032b96b05e51 109 }
xx316 1:032b96b05e51 110
xx316 1:032b96b05e51 111 /**
xx316 1:032b96b05e51 112 * Get pending char. Either from the high priority buffer (set with setPending), or from the
xx316 1:032b96b05e51 113 * circular buffer.
xx316 1:032b96b05e51 114 *
xx316 1:032b96b05e51 115 * @param data Filled with the pending data, when present
xx316 1:032b96b05e51 116 * @return true if data was filled
xx316 1:032b96b05e51 117 */
xx316 1:032b96b05e51 118 bool getPending(uint8_t &data)
xx316 1:032b96b05e51 119 {
xx316 1:032b96b05e51 120 if (dataIsPending) {
xx316 1:032b96b05e51 121 dataIsPending = false;
xx316 1:032b96b05e51 122 data = pendingData;
xx316 1:032b96b05e51 123 return true;
xx316 1:032b96b05e51 124 }
xx316 1:032b96b05e51 125
xx316 1:032b96b05e51 126 return pop(data);
xx316 1:032b96b05e51 127 }
xx316 1:032b96b05e51 128
xx316 1:032b96b05e51 129 bool isSomethingPending(void)
xx316 1:032b96b05e51 130 {
xx316 1:032b96b05e51 131 return dataIsPending || keyUpIsPending || !empty();
xx316 1:032b96b05e51 132 }
xx316 1:032b96b05e51 133
xx316 1:032b96b05e51 134 /**
xx316 1:032b96b05e51 135 * Signal that a keyUp report is pending. This means that a character has successfully been
xx316 1:032b96b05e51 136 * sent, but the subsequent keyUp report failed. This report is of highest priority than the
xx316 1:032b96b05e51 137 * next character.
xx316 1:032b96b05e51 138 */
xx316 1:032b96b05e51 139 void setKeyUpPending(void)
xx316 1:032b96b05e51 140 {
xx316 1:032b96b05e51 141 keyUpIsPending = true;
xx316 1:032b96b05e51 142 }
xx316 1:032b96b05e51 143
xx316 1:032b96b05e51 144 /**
xx316 1:032b96b05e51 145 * Signal that no high-priority report is pending anymore, we can go back to the normal queue.
xx316 1:032b96b05e51 146 */
xx316 1:032b96b05e51 147 void clearKeyUpPending(void)
xx316 1:032b96b05e51 148 {
xx316 1:032b96b05e51 149 keyUpIsPending = false;
xx316 1:032b96b05e51 150 }
xx316 1:032b96b05e51 151
xx316 1:032b96b05e51 152 bool isKeyUpPending(void)
xx316 1:032b96b05e51 153 {
xx316 1:032b96b05e51 154 return keyUpIsPending;
xx316 1:032b96b05e51 155 }
xx316 1:032b96b05e51 156
xx316 1:032b96b05e51 157 protected:
xx316 1:032b96b05e51 158 bool dataIsPending;
xx316 1:032b96b05e51 159 uint8_t pendingData;
xx316 1:032b96b05e51 160 bool keyUpIsPending;
xx316 1:032b96b05e51 161 };
xx316 1:032b96b05e51 162
xx316 1:032b96b05e51 163
xx316 1:032b96b05e51 164 /**
xx316 1:032b96b05e51 165 * @class KeyboardService
xx316 1:032b96b05e51 166 * @brief HID-over-Gatt keyboard service
xx316 1:032b96b05e51 167 *
xx316 1:032b96b05e51 168 * Send keyboard reports over BLE. Users should rely on the high-level functions provided by the
xx316 1:032b96b05e51 169 * Stream API. Because we can't send batches of HID reports, we store pending keys in a circular
xx316 1:032b96b05e51 170 * buffer and rely on the report ticker to spread them over time.
xx316 1:032b96b05e51 171 *
xx316 1:032b96b05e51 172 * @code
xx316 1:032b96b05e51 173 * BLE ble;
xx316 1:032b96b05e51 174 * KeyboardService kbd(ble);
xx316 1:032b96b05e51 175 *
xx316 1:032b96b05e51 176 * void once_connected_and_paired_callback(void)
xx316 1:032b96b05e51 177 * {
xx316 1:032b96b05e51 178 * // Sequentially send keys 'Shift'+'h', 'e', 'l', 'l', 'o', '!' and <enter>
xx316 1:032b96b05e51 179 * kbd.printf("Hello!\n");
xx316 1:032b96b05e51 180 * }
xx316 1:032b96b05e51 181 * @endcode
xx316 1:032b96b05e51 182 */
xx316 1:032b96b05e51 183 class KeyboardService : public HIDServiceBase, public Stream
xx316 1:032b96b05e51 184 {
xx316 1:032b96b05e51 185 public:
xx316 1:032b96b05e51 186 KeyboardService(BLE &_ble) :
xx316 1:032b96b05e51 187 HIDServiceBase(_ble,
xx316 1:032b96b05e51 188 KEYBOARD_REPORT_MAP, sizeof(KEYBOARD_REPORT_MAP),
xx316 1:032b96b05e51 189 inputReport = emptyInputReportData,
xx316 1:032b96b05e51 190 outputReport = outputReportData,
xx316 1:032b96b05e51 191 featureReport = NULL,
xx316 1:032b96b05e51 192 inputReportLength = sizeof(inputReportData),
xx316 1:032b96b05e51 193 outputReportLength = sizeof(outputReportData),
xx316 1:032b96b05e51 194 featureReportLength = 0,
xx316 1:032b96b05e51 195 reportTickerDelay = 24),
xx316 1:032b96b05e51 196 failedReports(0)
xx316 1:032b96b05e51 197 {
xx316 1:032b96b05e51 198 }
xx316 1:032b96b05e51 199
xx316 1:032b96b05e51 200 virtual void onConnection(const Gap::ConnectionCallbackParams_t *params)
xx316 1:032b96b05e51 201 {
xx316 1:032b96b05e51 202 HIDServiceBase::onConnection(params);
xx316 1:032b96b05e51 203
xx316 1:032b96b05e51 204 /* Drain buffer, in case we've been disconnected while transmitting */
xx316 1:032b96b05e51 205 if (!reportTickerIsActive && keyBuffer.isSomethingPending())
xx316 1:032b96b05e51 206 startReportTicker();
xx316 1:032b96b05e51 207 }
xx316 1:032b96b05e51 208
xx316 1:032b96b05e51 209 virtual void onDisconnection(const Gap::DisconnectionCallbackParams_t *params)
xx316 1:032b96b05e51 210 {
xx316 1:032b96b05e51 211 stopReportTicker();
xx316 1:032b96b05e51 212 HIDServiceBase::onDisconnection(params);
xx316 1:032b96b05e51 213 }
xx316 1:032b96b05e51 214
xx316 1:032b96b05e51 215 /**
xx316 1:032b96b05e51 216 * Send raw report. Should only be called by sendCallback.
xx316 1:032b96b05e51 217 */
xx316 1:032b96b05e51 218 virtual ble_error_t send(const report_t report)
xx316 1:032b96b05e51 219 {
xx316 1:032b96b05e51 220 static unsigned int consecutiveFailures = 0;
xx316 1:032b96b05e51 221 ble_error_t ret = HIDServiceBase::send(report);
xx316 1:032b96b05e51 222
xx316 1:032b96b05e51 223 /*
xx316 1:032b96b05e51 224 * Wait until a buffer is available (onDataSent)
xx316 1:032b96b05e51 225 * TODO. This won't work, because BUSY error is not only returned when we're short of
xx316 1:032b96b05e51 226 * notification buffers, but in other cases as well (e.g. when disconnected). We need to
xx316 1:032b96b05e51 227 * find a reliable way of knowing when we actually need to wait for onDataSent to be called.
xx316 1:032b96b05e51 228 if (ret == BLE_STACK_BUSY)
xx316 1:032b96b05e51 229 stopReportTicker();
xx316 1:032b96b05e51 230 */
xx316 1:032b96b05e51 231 if (ret == BLE_STACK_BUSY)
xx316 1:032b96b05e51 232 consecutiveFailures++;
xx316 1:032b96b05e51 233 else
xx316 1:032b96b05e51 234 consecutiveFailures = 0;
xx316 1:032b96b05e51 235
xx316 1:032b96b05e51 236 if (consecutiveFailures > 20) {
xx316 1:032b96b05e51 237 /*
xx316 1:032b96b05e51 238 * We're not transmitting anything anymore. Might as well avoid overloading the
xx316 1:032b96b05e51 239 * system in case it can magically fix itself. Ticker will start again on next _putc
xx316 1:032b96b05e51 240 * call. It could also be started on next connection, but we can't register a callback
xx316 1:032b96b05e51 241 * for that, currently.
xx316 1:032b96b05e51 242 */
xx316 1:032b96b05e51 243 stopReportTicker();
xx316 1:032b96b05e51 244 consecutiveFailures = 0;
xx316 1:032b96b05e51 245 }
xx316 1:032b96b05e51 246
xx316 1:032b96b05e51 247 return ret;
xx316 1:032b96b05e51 248 }
xx316 1:032b96b05e51 249
xx316 1:032b96b05e51 250 /**
xx316 1:032b96b05e51 251 * Send an empty report, representing keyUp event
xx316 1:032b96b05e51 252 */
xx316 1:032b96b05e51 253 ble_error_t keyUpCode(void)
xx316 1:032b96b05e51 254 {
xx316 1:032b96b05e51 255 return send(emptyInputReportData);
xx316 1:032b96b05e51 256 }
xx316 1:032b96b05e51 257
xx316 1:032b96b05e51 258 /**
xx316 1:032b96b05e51 259 * Send a character, defined by a modifier (CTRL, SHIFT, ALT) and the key
xx316 1:032b96b05e51 260 *
xx316 1:032b96b05e51 261 * @param key Character to send (as defined in USB HID Usage Tables)
xx316 1:032b96b05e51 262 * @param modifier Optional modifiers (logical OR of enum MODIFIER_KEY)
xx316 1:032b96b05e51 263 *
xx316 1:032b96b05e51 264 * @returns BLE_ERROR_NONE on success, or an error code otherwise.
xx316 1:032b96b05e51 265 */
xx316 1:032b96b05e51 266 ble_error_t keyDownCode(uint8_t key, uint8_t modifier)
xx316 1:032b96b05e51 267 {
xx316 1:032b96b05e51 268 inputReportData[0] = modifier;
xx316 1:032b96b05e51 269 inputReportData[2] = keymap[key].usage;
xx316 1:032b96b05e51 270
xx316 1:032b96b05e51 271 return send(inputReportData);
xx316 1:032b96b05e51 272 }
xx316 1:032b96b05e51 273
xx316 1:032b96b05e51 274 /**
xx316 1:032b96b05e51 275 * Push a key on the internal FIFO
xx316 1:032b96b05e51 276 *
xx316 1:032b96b05e51 277 * @param c ASCII character to send
xx316 1:032b96b05e51 278 *
xx316 1:032b96b05e51 279 * @returns 0 on success, or ENOMEM when the FIFO is full.
xx316 1:032b96b05e51 280 */
xx316 1:032b96b05e51 281 virtual int _putc(int c) {
xx316 1:032b96b05e51 282 if (keyBuffer.full()) {
xx316 1:032b96b05e51 283 return ENOMEM;
xx316 1:032b96b05e51 284 }
xx316 1:032b96b05e51 285
xx316 1:032b96b05e51 286 keyBuffer.push((unsigned char)c);
xx316 1:032b96b05e51 287
xx316 1:032b96b05e51 288 if (!reportTickerIsActive)
xx316 1:032b96b05e51 289 startReportTicker();
xx316 1:032b96b05e51 290
xx316 1:032b96b05e51 291 return 0;
xx316 1:032b96b05e51 292 }
xx316 1:032b96b05e51 293
xx316 1:032b96b05e51 294 uint8_t lockStatus() {
xx316 1:032b96b05e51 295 // TODO: implement numlock/capslock/scrolllock
xx316 1:032b96b05e51 296 return 0;
xx316 1:032b96b05e51 297 }
xx316 1:032b96b05e51 298
xx316 1:032b96b05e51 299 /**
xx316 1:032b96b05e51 300 * Pop a key from the internal FIFO, and attempt to send it over BLE
xx316 1:032b96b05e51 301 */
xx316 1:032b96b05e51 302 virtual void sendCallback(void) {
xx316 1:032b96b05e51 303 ble_error_t ret;
xx316 1:032b96b05e51 304 uint8_t c;
xx316 1:032b96b05e51 305
xx316 1:032b96b05e51 306 if (!keyBuffer.isSomethingPending()) {
xx316 1:032b96b05e51 307 /* Stop until the next call to putc */
xx316 1:032b96b05e51 308 stopReportTicker();
xx316 1:032b96b05e51 309 return;
xx316 1:032b96b05e51 310 }
xx316 1:032b96b05e51 311
xx316 1:032b96b05e51 312 if (!keyBuffer.isKeyUpPending()) {
xx316 1:032b96b05e51 313 bool hasData = keyBuffer.getPending(c);
xx316 1:032b96b05e51 314
xx316 1:032b96b05e51 315 /*
xx316 1:032b96b05e51 316 * If something is pending and is not a keyUp, getPending *must* return something. The
xx316 1:032b96b05e51 317 * following is only a sanity check.
xx316 1:032b96b05e51 318 */
xx316 1:032b96b05e51 319 MBED_ASSERT(hasData);
xx316 1:032b96b05e51 320
xx316 1:032b96b05e51 321 if (hasData) {
xx316 1:032b96b05e51 322 ret = keyDownCode(c, keymap[c].modifier);
xx316 1:032b96b05e51 323 if (ret) {
xx316 1:032b96b05e51 324 keyBuffer.setPending(c);
xx316 1:032b96b05e51 325 failedReports++;
xx316 1:032b96b05e51 326 return;
xx316 1:032b96b05e51 327 }
xx316 1:032b96b05e51 328 }
xx316 1:032b96b05e51 329 }
xx316 1:032b96b05e51 330
xx316 1:032b96b05e51 331 ret = keyUpCode();
xx316 1:032b96b05e51 332 if (ret) {
xx316 1:032b96b05e51 333 keyBuffer.setKeyUpPending();
xx316 1:032b96b05e51 334 failedReports++;
xx316 1:032b96b05e51 335 } else {
xx316 1:032b96b05e51 336 keyBuffer.clearKeyUpPending();
xx316 1:032b96b05e51 337 }
xx316 1:032b96b05e51 338 }
xx316 1:032b96b05e51 339
xx316 1:032b96b05e51 340 /**
xx316 1:032b96b05e51 341 * Restart report ticker if it was disabled, after too many consecutive failures.
xx316 1:032b96b05e51 342 *
xx316 1:032b96b05e51 343 * This is called by the BLE stack.
xx316 1:032b96b05e51 344 *
xx316 1:032b96b05e51 345 * @param count Number of reports (notifications) sent
xx316 1:032b96b05e51 346 */
xx316 1:032b96b05e51 347 virtual void onDataSent(unsigned count)
xx316 1:032b96b05e51 348 {
xx316 1:032b96b05e51 349 if (!reportTickerIsActive && keyBuffer.isSomethingPending())
xx316 1:032b96b05e51 350 startReportTicker();
xx316 1:032b96b05e51 351 }
xx316 1:032b96b05e51 352
xx316 1:032b96b05e51 353 unsigned long failedReports;
xx316 1:032b96b05e51 354
xx316 1:032b96b05e51 355 protected:
xx316 1:032b96b05e51 356 virtual int _getc() {
xx316 1:032b96b05e51 357 return 0;
xx316 1:032b96b05e51 358 }
xx316 1:032b96b05e51 359
xx316 1:032b96b05e51 360 protected:
xx316 1:032b96b05e51 361 KeyBuffer keyBuffer;
xx316 1:032b96b05e51 362
xx316 1:032b96b05e51 363 //GattCharacteristic boot_keyboard_input_report;
xx316 1:032b96b05e51 364 //GattCharacteristic boot_keyboard_output_report;
xx316 1:032b96b05e51 365 };
xx316 1:032b96b05e51 366
xx316 1:032b96b05e51 367