diff --git a/Makefile b/Makefile index e3ac3e5d..87ade339 100644 --- a/Makefile +++ b/Makefile @@ -103,7 +103,7 @@ SRC_C += monocle-core/monocle-drivers.c SRC_C += monocle-core/monocle-startup.c SRC_C += mphalport.c -SRC_C += micropython/extmod/moduasyncio.c +SRC_C += micropython/extmod/modasyncio.c SRC_C += micropython/extmod/modbinascii.c SRC_C += micropython/extmod/modhashlib.c SRC_C += micropython/extmod/modjson.c @@ -120,11 +120,11 @@ SRC_C += micropython/extmod/vfs_reader.c SRC_C += micropython/extmod/vfs.c SRC_C += modules/bluetooth.c SRC_C += modules/camera.c -SRC_C += modules/compression.c SRC_C += modules/device.c SRC_C += modules/display.c SRC_C += modules/fpga.c SRC_C += modules/led.c +SRC_C += modules/microphone.c SRC_C += modules/rtt.c SRC_C += modules/storage.c SRC_C += modules/touch.c diff --git a/micropython b/micropython index 47dc7d01..813d559b 160000 --- a/micropython +++ b/micropython @@ -1 +1 @@ -Subproject commit 47dc7d0130d583ce3c9426a82eabe0473ec1cfa5 +Subproject commit 813d559bc098eeaa1c6e0fa1deff92e666c0b458 diff --git a/modules/_test.py b/modules/_test.py index e731aee2..abc59585 100644 --- a/modules/_test.py +++ b/modules/_test.py @@ -187,12 +187,17 @@ def camera_module(): def microphone_module(): __test("microphone.record()", None) - __test("microphone.record(seconds=6.5)", None) + __test("microphone.record(seconds=4)", None) + __test("microphone.record(seconds=4.5)", None) + __test("microphone.record(sample_rate=4000)", ValueError) + __test("microphone.record(sample_rate=8000)", None) + __test("microphone.record(sample_rate=16000)", None) + __test("microphone.record(bit_depth=4)", ValueError) + __test("microphone.record(bit_depth=8)", None) + __test("microphone.record(bit_depth=16)", None) time.sleep(0.5) - __test("len(microphone.read())", 127) - __test("len(microphone.read(samples=10))", 10) - __test("len(microphone.read(samples=128))", ValueError) - __test("microphone.compress([23432,24399,24300,24500])", b"[\x88") + __test("len(microphone.read(127))", 254) + __test("len(microphone.read(128))", ValueError) def touch_module(): diff --git a/modules/compression.c b/modules/compression.c deleted file mode 100644 index 7b7836da..00000000 --- a/modules/compression.c +++ /dev/null @@ -1,73 +0,0 @@ -/* - * This file is part of the MicroPython for Monocle project: - * https://github.com/brilliantlabsAR/monocle-micropython - * - * Authored by: Josuah Demangeon (me@josuah.net) - * Raj Nakarja / Brilliant Labs Ltd. (raj@itsbrilliant.co) - * - * ISC Licence - * - * Copyright © 2023 Brilliant Labs Ltd. - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ - -#include "monocle.h" -#include "py/runtime.h" - -STATIC mp_obj_t delta_encode(mp_obj_t input_object) -{ - if (!mp_obj_is_type(input_object, &mp_type_list)) - { - mp_raise_msg_varg(&mp_type_TypeError, - MP_ERROR_TEXT("data must be a list")); - } - - size_t list_length; - mp_obj_t *list_data; - mp_obj_list_get(input_object, &list_length, &list_data); - - uint8_t bytes[list_length + 1]; - - bytes[0] = (uint8_t)(mp_obj_get_int(list_data[0]) >> 8); - bytes[1] = (uint8_t)mp_obj_get_int(list_data[0]); - - size_t i; - for (i = 1; i < list_length; i++) - { - mp_int_t difference = mp_obj_get_int(list_data[i]) - - mp_obj_get_int(list_data[i - 1]); - - if (difference > 127 || difference < -127) - { - break; - } - - bytes[i + 1] = difference; - } - - return mp_obj_new_bytes(bytes, i + 1); -} -STATIC MP_DEFINE_CONST_FUN_OBJ_1(delta_encode_obj, delta_encode); - -STATIC const mp_rom_map_elem_t compression_module_globals_table[] = { - - {MP_ROM_QSTR(MP_QSTR_delta_encode), MP_ROM_PTR(&delta_encode_obj)}, -}; -STATIC MP_DEFINE_CONST_DICT(compression_module_globals, compression_module_globals_table); - -const mp_obj_module_t compression_module = { - .base = {&mp_type_module}, - .globals = (mp_obj_dict_t *)&compression_module_globals, -}; -MP_REGISTER_MODULE(MP_QSTR__compression, compression_module); diff --git a/modules/display.py b/modules/display.py index 5551a5a1..41423831 100644 --- a/modules/display.py +++ b/modules/display.py @@ -288,40 +288,6 @@ def color(*args): arg.color(args[-1]) -def text_get_overlapping_y(l): - if len(l) == 0: - return - l = sorted(l, key=lambda obj: obj.y) - prev = l[0] - for obj in l[1:]: - if obj.y < prev.y + FONT_HEIGHT: - return (prev, obj) - prev = obj - - -def text_get_overlapping_xy(base, l): - if len(l) == 0: - return - sub = [base] - for obj in l: - if obj.x < base.x + base.width(base.string): - # Some overlapping on x coordinates, accumulate the row - sub.append(obj) - else: - # Since the l is sorted, we can stop checking here - break - - # now also check the y coordinate for all the potential clashes - return text_get_overlapping_y(sub) - - -def text_get_overlapping(l): - for i in range(len(l)): - overlapping = text_get_overlapping_xy(l[i], l[i + 1:]) - if overlapping is not None: - return overlapping - - def update_colors(addr, l): # new buffer for the FPGA API, starting with address 0x0000 buffer = bytearray(2) @@ -354,14 +320,33 @@ def update_colors(addr, l): def show_fbtext(l): global fbtext_addr + # Make sure there was enough time to start the FPGA engine + while time.ticks_ms() < 1000: + pass + update_colors(0x4502, l) + # Text has no wrapper, we implement it locally. # See https://streamlogic.io/docs/reify/nodes/#fbtext buffer = bytearray(struct.pack(">H", fbtext_addr)) l = sorted(l, key=lambda obj: obj.x) - overlapping = text_get_overlapping(l) - if overlapping is not None: - raise TextOverlapError(f"{overlapping[0]} overlaps with {overlapping[1]}") + + # Check for overlapping text + def box(obj): + x2 = obj.x + FONT_WIDTH * len(obj.string) + y2 = obj.y + FONT_HEIGHT + return obj.x, x2, obj.y, y2 + for a in l: + ax1, ax2, ay1, ay2 = box(a) + for b in l: + if a is b: + continue + bx1, bx2, by1, by2 = box(b) + if ax1 <= bx2 and ax2 >= bx1: + if ay1 <= by2 and ay2 >= by1: + raise TextOverlapError(f"{a} overlaps with {b}") + + # Render the text for obj in l: obj.fbtext(buffer) if len(buffer) > 0: @@ -370,11 +355,12 @@ def show_fbtext(l): fpga.write(0x4503, buffer + b"\xFF\xFF\xFF") fbtext_addr += FBTEXT_PAGE_SIZE fbtext_addr %= FBTEXT_PAGE_SIZE * FBTEXT_NUM_PAGES - time.sleep_ms(20) # ensure the buffer swap has happened + time.sleep_ms(100) # ensure the buffer swap has happened def show_vgr2d(l): update_colors(0x4402, l) + # 0 is the address of the frame in the framebuffer in use. # See https://streamlogic.io/docs/reify/nodes/#fbgraphics # Offset: active display offset in buffer used if double buffering diff --git a/modules/frozen-manifest.py b/modules/frozen-manifest.py index 97d01551..35d00ea3 100644 --- a/modules/frozen-manifest.py +++ b/modules/frozen-manifest.py @@ -27,7 +27,6 @@ module("_test.py") module("camera.py") module("display.py") -module("microphone.py") module("update.py") -include("$(MPY_DIR)/extmod/uasyncio/manifest.py") +include("$(MPY_DIR)/extmod/asyncio/manifest.py") diff --git a/modules/microphone.c b/modules/microphone.c new file mode 100644 index 00000000..6e3e2063 --- /dev/null +++ b/modules/microphone.c @@ -0,0 +1,223 @@ +/* + * This file is part of the MicroPython for Monocle project: + * https://github.com/brilliantlabsAR/monocle-micropython + * + * Authored by: Josuah Demangeon (me@josuah.net) + * Raj Nakarja / Brilliant Labs Ltd. (raj@itsbrilliant.co) + * + * ISC Licence + * + * Copyright © 2023 Brilliant Labs Ltd. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + * PERFORMANCE OF THIS SOFTWARE. + */ + +#include +#include "monocle.h" +#include "py/runtime.h" + +static uint8_t microphone_bit_depth = 16; + +static inline void microphone_fpga_read(uint16_t address, uint8_t *buffer, size_t length) +{ + uint8_t address_bytes[2] = {(uint8_t)(address >> 8), (uint8_t)address}; + + monocle_spi_write(FPGA, address_bytes, 2, true); + + // Dump the data into a dummy buffer if a buffer isn't provided + if (buffer == NULL) + { + uint8_t dummy_buffer[254]; + monocle_spi_read(FPGA, dummy_buffer, length, false); + return; + } + + monocle_spi_read(FPGA, buffer, length, false); +} + +static inline void microphone_fpga_write(uint16_t address, uint8_t *buffer, size_t length) +{ + uint8_t address_bytes[2] = {(uint8_t)(address >> 8), (uint8_t)address}; + + if (buffer == NULL || length == 0) + { + monocle_spi_write(FPGA, address_bytes, 2, false); + return; + } + + monocle_spi_write(FPGA, address_bytes, 2, true); + monocle_spi_write(FPGA, buffer, length, false); +} + +static size_t microphone_bytes_available(void) +{ + uint8_t available_bytes[2] = {0, 0}; + microphone_fpga_read(0x5801, available_bytes, sizeof(available_bytes)); + size_t available = (available_bytes[0] << 8 | available_bytes[1]) * 2; + + // Cap to 254 due to SPI DMA limit + if (available > 254) + { + available = 254; + } + + return available; +} + +STATIC mp_obj_t microphone_init(void) +{ + uint8_t fpga_image[4]; + uint8_t module_status[2]; + + microphone_fpga_read(0x0001, fpga_image, sizeof(fpga_image)); + microphone_fpga_read(0x5800, module_status, sizeof(module_status)); + + if (((module_status[0] & 0x10) != 16) || + memcmp(fpga_image, "Mncl", sizeof(fpga_image))) + { + mp_raise_NotImplementedError( + MP_ERROR_TEXT("microphone driver not found on FPGA")); + } + + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_0(microphone_init_obj, microphone_init); + +STATIC mp_obj_t microphone_record(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) +{ + static const mp_arg_t allowed_args[] = { + {MP_QSTR_sample_rate, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 16000}}, + {MP_QSTR_bit_depth, MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 16}}, + {MP_QSTR_seconds, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NEW_SMALL_INT(5)}}}; + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + // Flush existing data + while (true) + { + size_t available = microphone_bytes_available(); + + if (available == 0) + { + break; + } + + microphone_fpga_read(0x5807, NULL, available); + } + + // Check the given sample rate + mp_int_t sample_rate = args[0].u_int; + + if (sample_rate != 16000 && sample_rate != 8000) + { + mp_raise_ValueError( + MP_ERROR_TEXT("sample rate must be either 16000 or 8000")); + } + + // Check the currently set sample rate on the FPGA + uint8_t status_byte; + microphone_fpga_read(0x0800, &status_byte, sizeof(status_byte)); + + // Toggle the sample rate if required + if (((status_byte & 0x04) == 0x00 && sample_rate == 8000) || + ((status_byte & 0x04) == 0x04 && sample_rate == 16000)) + { + microphone_fpga_write(0x0808, NULL, 0); + } + + // Check and set bit depth to the global variable + mp_int_t bit_depth = args[1].u_int; + if (bit_depth != 16 && bit_depth != 8) + { + mp_raise_ValueError( + MP_ERROR_TEXT("bit depth must be either 16 or 8")); + } + microphone_bit_depth = bit_depth; + + // Set the block size and request a number of blocks corresponding to seconds + float block_size; + sample_rate == 16000 ? (block_size = 0.02) : (block_size = 0.04); + + uint16_t blocks = (uint16_t)(mp_obj_get_float(args[2].u_obj) / block_size); + uint8_t blocks_bytes[] = {blocks >> 8, blocks}; + microphone_fpga_write(0x0802, blocks_bytes, sizeof(blocks)); + + // Trigger capture + microphone_fpga_write(0x0803, NULL, 0); + + return mp_const_none; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_KW(microphone_record_obj, 0, microphone_record); + +STATIC mp_obj_t microphone_stop(void) +{ + return mp_const_notimplemented; +} +STATIC MP_DEFINE_CONST_FUN_OBJ_0(microphone_stop_obj, microphone_stop); + +STATIC mp_obj_t microphone_read(mp_obj_t samples) +{ + if (mp_obj_get_int(samples) > 127) + { + mp_raise_ValueError( + MP_ERROR_TEXT("only 127 samples may be read at a time")); + } + + size_t available = microphone_bytes_available(); + + if (available == 0) + { + return mp_const_none; + } + + if (mp_obj_get_int(samples) * 2 < available) + { + available = mp_obj_get_int(samples) * 2; + } + + uint8_t buffer[available]; + microphone_fpga_read(0x5807, buffer, sizeof(buffer)); + + // If 16 bit data, return this buffer + if (microphone_bit_depth == 16) + { + return mp_obj_new_bytes(buffer, sizeof(buffer)); + } + + // Otherwise create a scaled version for 8 bit + uint8_t small_buffer[sizeof(buffer) / 2]; + for (size_t i = 0; i < sizeof(buffer) / 2; i++) + { + uint16_t data16 = buffer[i * 2] << 8 | buffer[i + 2 + 1]; + small_buffer[i] = data16 >> 8; + } + + return mp_obj_new_bytes(small_buffer, sizeof(small_buffer)); +} +STATIC MP_DEFINE_CONST_FUN_OBJ_1(microphone_read_obj, microphone_read); + +STATIC const mp_rom_map_elem_t microphone_module_globals_table[] = { + + {MP_ROM_QSTR(MP_QSTR___init__), MP_ROM_PTR(µphone_init_obj)}, + {MP_ROM_QSTR(MP_QSTR_record), MP_ROM_PTR(µphone_record_obj)}, + {MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(µphone_stop_obj)}, + {MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(µphone_read_obj)}, +}; +STATIC MP_DEFINE_CONST_DICT(microphone_module_globals, microphone_module_globals_table); + +const mp_obj_module_t microphone_module = { + .base = {&mp_type_module}, + .globals = (mp_obj_dict_t *)µphone_module_globals, +}; +MP_REGISTER_MODULE(MP_QSTR_microphone, microphone_module); diff --git a/modules/microphone.py b/modules/microphone.py deleted file mode 100644 index f6952fce..00000000 --- a/modules/microphone.py +++ /dev/null @@ -1,102 +0,0 @@ -# -# This file is part of the MicroPython for Monocle project: -# https://github.com/brilliantlabsAR/monocle-micropython -# -# Authored by: Josuah Demangeon (me@josuah.net) -# Raj Nakarja / Brilliant Labs Ltd. (raj@itsbrilliant.co) -# -# ISC Licence -# -# Copyright © 2023 Brilliant Labs Ltd. -# -# Permission to use, copy, modify, and/or distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -# PERFORMANCE OF THIS SOFTWARE. -# - -import fpga -import struct -import _compression as __compression - - -__image = fpga.read(0x0001, 4) -__status = fpga.read(0x5800, 1)[0] & 0x10 -if __status != 16 or __image != b"Mncl": - raise (NotImplementedError("microphone driver not found on FPGA")) - - -def __flush(): - count = 0 - while True: - available = 2 * int.from_bytes(fpga.read(0x5801, 2), "big") - if available == 0: - break - fpga.read(0x5807, min(254, available)) - count += min(254, available) - - -def record(sample_rate=16000, seconds=1.0): - # TODO possible to pass sample rate to FPGA? - - __flush() - - # Set window size. Resolution is 20ms - # TODO possible to omit window size? This allows explicit stop/start - n = int(seconds / 0.02) - fpga.write(0x0802, int.to_bytes(n, 2, "big")) - - # Trigger capture - fpga.write(0x0803, "") - - -def __read_raw(samples=-1): - if samples > 127: - raise (ValueError("only 127 samples may be read at a time")) - - available = 2 * int.from_bytes(fpga.read(0x5801, 2), "big") - - if available == 0: - return None - - available = min(available, 254) - - if samples == -1: - data = fpga.read(0x5807, available) - else: - data = fpga.read(0x5807, min(samples * 2, available)) - - return data - - -def read(samples=-1): - byte_data = __read_raw(samples) - - if byte_data == None: - return None - - int16_list = [] - - for i in range(len(byte_data) / 2): - int16_list.append(struct.unpack(">h", byte_data[i * 2 : i * 2 + 2])[0]) - - return int16_list - - -def stop(): - # TODO send stop command - pass - - -def compress(data): - return __compression.delta_encode(data) - - -# TODO add callback handler when keyword detection is available diff --git a/mpconfigport.h b/mpconfigport.h index ba2146da..47274312 100644 --- a/mpconfigport.h +++ b/mpconfigport.h @@ -41,6 +41,8 @@ #define MICROPY_PERSISTENT_CODE_LOAD (1) +#define MICROPY_MODULE_BUILTIN_INIT (1) + #define MICROPY_ENABLE_SCHEDULER (1) #define MICROPY_COMP_MODULE_CONST (1) @@ -49,7 +51,7 @@ #define MICROPY_ERROR_REPORTING (MICROPY_ERROR_REPORTING_DETAILED) -#define MICROPY_PY_UASYNCIO (1) +#define MICROPY_PY_ASYNCIO (1) #define MICROPY_MODULE_WEAK_LINKS (1) diff --git a/tools/serial_console.py b/tools/serial_console.py index 64d2a41e..0e10f13b 100755 --- a/tools/serial_console.py +++ b/tools/serial_console.py @@ -51,9 +51,6 @@ async def repl_terminal(): remote device. Any data received from the device is printed to stdout. """ - # opens sandard output in binary mode - stdout = os.fdopen(1, 'wb') - def match_repl_uuid(device: BLEDevice, adv: AdvertisementData): # This assumes that the device includes the UART service UUID in the # advertising data. This test may need to be adjusted depending on the @@ -61,12 +58,6 @@ def match_repl_uuid(device: BLEDevice, adv: AdvertisementData): sys.stderr.write(f"uuids={adv.service_uuids}\n") return UART_SERVICE_UUID.lower() in adv.service_uuids - device = await BleakScanner.find_device_by_filter(match_repl_uuid) - - if device is None: - sys.stderr.write("no matching device found\n") - sys.exit(1) - def handle_disconnect(_: BleakClient): sys.stderr.write("\r\nDevice was disconnected.\r\n") @@ -91,6 +82,20 @@ def prompt(): tty.setraw(0) return line + stdout = os.fdopen(1, 'wb') + + device = await BleakScanner.find_device_by_filter(match_repl_uuid) + if device is None: + sys.stderr.write("no matching device found\n") + sys.exit(1) + else: + sys.stderr.write(f"connected\n") + sys.stderr.write('Ctrl-D: Reboot in normal mode\n") + sys.stderr.write('Ctrl-\\: Reboot in safe mode\n") + sys.stderr.write('Ctrl-C: Cancel ongoing script\n") + sys.stderr.write('Ctrl-E, "code", Ctrl-D: paste mode\n") + sys.stderr.write('Ctrl-V, "text", Enter: send to raw Bluetooth service\n') + async with BleakClient(device, disconnected_callback=handle_disconnect) as client: await client.start_notify(UART_TX_CHAR_UUID, handle_repl_rx) await client.start_notify(DATA_TX_CHAR_UUID, handle_data_rx) @@ -105,8 +110,6 @@ def prompt(): if sys.stdin.isatty(): tty.setraw(0) - sys.stderr.write('Ctrl-V + "input text" + Enter: data to raw service\r\n') - # Infinite loop to read the input character until the end while True: ch = await loop.run_in_executor(None, sys.stdin.buffer.read, 1) @@ -116,7 +119,7 @@ def prompt(): sys.stderr.write(f'TX: ') sys.stderr.flush() line = await loop.run_in_executor(None, prompt) - await client.write_gatt_char(data_rx_char, line) + await client.write_gatt_char(data_rx_char, line.rstrip()) else: await client.write_gatt_char(repl_rx_char, ch)