diff --git a/CHANGELOG.md b/CHANGELOG.md index 617951345..bdeb6e0c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,8 @@ - Bluray/DVD Universal Remote (#250 by @jaylikesbunda) - Option to "Load from Library File" for Universal Remotes (#255 by @zxkmm) - Updater: New Yappy themed icon while updating (#253 by @the1anonlypr3 & @Kuronons & @nescap) +- JS: + - New `i2c` module (#259 by @jamisonderek) - BadKB: - OFW: Add linux/gnome badusb demo files (by @thomasnemer) - Add older qFlipper install demos for windows and macos (by @DXVVAY & @grugnoymeme) diff --git a/applications/system/js_app/application.fam b/applications/system/js_app/application.fam index 65fafed76..8c7cdf9ad 100644 --- a/applications/system/js_app/application.fam +++ b/applications/system/js_app/application.fam @@ -208,3 +208,11 @@ App( requires=["js_app"], sources=["modules/js_usbdisk/*.c"], ) + +App( + appid="js_i2c", + apptype=FlipperAppType.PLUGIN, + entry_point="js_i2c_ep", + requires=["js_app"], + sources=["modules/js_i2c.c"], +) diff --git a/applications/system/js_app/examples/apps/Scripts/Examples/i2c.js b/applications/system/js_app/examples/apps/Scripts/Examples/i2c.js new file mode 100644 index 000000000..456b44ccd --- /dev/null +++ b/applications/system/js_app/examples/apps/Scripts/Examples/i2c.js @@ -0,0 +1,48 @@ +// Connect an 24C32N EEPROM to the I2C bus of the board. SDA=pin 15, SCL=pin 16, VCC=pin 9, GND=pin 8. +let i2c = require("i2c"); + +function i2c_find_first_device() { + let addr = -1; + for (let try_addr = 0; try_addr !== 0xff; try_addr++) { + if (i2c.isDeviceReady(try_addr, 5)) { + addr = try_addr; + break; + } + } + return addr; +} + +let addr = i2c_find_first_device(); +if (addr === -1) { + print("I2C device not found"); + print("Please connect a 24C32N EEPROM I2C device to the Flipper Zero."); + print("SDA=pin 15, SCL=pin 16, VCC=pin 9, GND=pin 8."); +} else { + print("I2C device found at address: " + addr.toString(16)); + delay(1000); + + // first two bytes are the start address (0x0000) + // the remaining bytes are the data to store. + // can also use Uint8Array([0x00, 0x00, ...]) as write parameter + i2c.write(addr, [0x00, 0x00, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47]); + while (i2c.isDeviceReady(addr, 9) === false) { + print("Waiting for device to be ready..."); + } + + // write the address to read from (we start at address 0x0001) + // read 3 bytes - 0x42, 0x43, 0x44 + let data_buf = i2c.writeRead(addr, [0x00, 0x01], 3, 100); + let data = Uint8Array(data_buf); + print("Read bytes: " + data.length.toString()); + for (let i = 0; i < data.length; i++) { + print("data[" + i.toString() + "] = " + data[i].toString(16)); + } + + // read two more bytes (0x45, 0x46) from current address + data_buf = i2c.read(addr, 2); + data = Uint8Array(data_buf); + print("Read bytes: " + data.length.toString()); + for (let i = 0; i < data.length; i++) { + print("data[" + i.toString() + "] = " + data[i].toString(16)); + } +} diff --git a/applications/system/js_app/modules/js_i2c.c b/applications/system/js_app/modules/js_i2c.c new file mode 100644 index 000000000..6f105142c --- /dev/null +++ b/applications/system/js_app/modules/js_i2c.c @@ -0,0 +1,280 @@ +#include "../js_modules.h" +#include + +static void ret_bad_args(struct mjs* mjs, const char* error) { + mjs_prepend_errorf(mjs, MJS_BAD_ARGS_ERROR, "%s", error); + mjs_return(mjs, MJS_UNDEFINED); +} + +static bool check_arg_count_range(struct mjs* mjs, size_t min_count, size_t max_count) { + size_t num_args = mjs_nargs(mjs); + if(num_args < min_count || num_args > max_count) { + ret_bad_args(mjs, "Wrong argument count"); + return false; + } + return true; +} + +static void js_i2c_is_device_ready(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 1, 2)) return; + + mjs_val_t addr_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(addr_arg)) { + ret_bad_args(mjs, "Addr must be a number"); + return; + } + uint32_t addr = mjs_get_int32(mjs, addr_arg); + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 1) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + furi_hal_i2c_acquire(&furi_hal_i2c_handle_external); + bool ready = furi_hal_i2c_is_device_ready(&furi_hal_i2c_handle_external, addr, timeout); + furi_hal_i2c_release(&furi_hal_i2c_handle_external); + + mjs_return(mjs, mjs_mk_boolean(mjs, ready)); +} + +static void js_i2c_write(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 2, 3)) return; + + mjs_val_t addr_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(addr_arg)) { + ret_bad_args(mjs, "Addr must be a number"); + return; + } + uint32_t addr = mjs_get_int32(mjs, addr_arg); + + mjs_val_t tx_buf_arg = mjs_arg(mjs, 1); + bool tx_buf_was_allocated = false; + uint8_t* tx_buf = NULL; + size_t tx_len = 0; + if(mjs_is_array(tx_buf_arg)) { + tx_len = mjs_array_length(mjs, tx_buf_arg); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + tx_buf = malloc(tx_len); + tx_buf_was_allocated = true; + for(size_t i = 0; i < tx_len; i++) { + mjs_val_t val = mjs_array_get(mjs, tx_buf_arg, i); + if(!mjs_is_number(val)) { + ret_bad_args(mjs, "Data array must contain only numbers"); + free(tx_buf); + return; + } + uint32_t byte_val = mjs_get_int32(mjs, val); + if(byte_val > 0xFF) { + ret_bad_args(mjs, "Data array values must be 0-255"); + free(tx_buf); + return; + } + tx_buf[i] = byte_val; + } + } else if(mjs_is_typed_array(tx_buf_arg)) { + mjs_val_t array_buf = tx_buf_arg; + if(mjs_is_data_view(tx_buf_arg)) { + array_buf = mjs_dataview_get_buf(mjs, tx_buf_arg); + } + tx_buf = (uint8_t*)mjs_array_buf_get_ptr(mjs, array_buf, &tx_len); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + } else { + ret_bad_args(mjs, "Data must be an array, arraybuf or dataview"); + return; + } + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 2) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 2); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + furi_hal_i2c_acquire(&furi_hal_i2c_handle_external); + bool result = furi_hal_i2c_tx(&furi_hal_i2c_handle_external, addr, tx_buf, tx_len, timeout); + furi_hal_i2c_release(&furi_hal_i2c_handle_external); + + if(tx_buf_was_allocated) free(tx_buf); + mjs_return(mjs, mjs_mk_boolean(mjs, result)); +} + +static void js_i2c_read(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 2, 3)) return; + + mjs_val_t addr_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(addr_arg)) { + ret_bad_args(mjs, "Addr must be a number"); + return; + } + uint32_t addr = mjs_get_int32(mjs, addr_arg); + + mjs_val_t rx_len_arg = mjs_arg(mjs, 1); + if(!mjs_is_number(rx_len_arg)) { + ret_bad_args(mjs, "Length must be a number"); + return; + } + size_t rx_len = mjs_get_int32(mjs, rx_len_arg); + if(rx_len == 0) { + ret_bad_args(mjs, "Length must not zero"); + return; + } + uint8_t* rx_buf = malloc(rx_len); + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 2) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 2); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + free(rx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + furi_hal_i2c_acquire(&furi_hal_i2c_handle_external); + bool result = furi_hal_i2c_rx(&furi_hal_i2c_handle_external, addr, rx_buf, rx_len, timeout); + furi_hal_i2c_release(&furi_hal_i2c_handle_external); + + mjs_val_t ret = MJS_UNDEFINED; + if(result) { + ret = mjs_mk_array_buf(mjs, (char*)rx_buf, rx_len); + } + free(rx_buf); + mjs_return(mjs, ret); +} + +static void js_i2c_write_read(struct mjs* mjs) { + if(!check_arg_count_range(mjs, 3, 4)) return; + + mjs_val_t addr_arg = mjs_arg(mjs, 0); + if(!mjs_is_number(addr_arg)) { + ret_bad_args(mjs, "Addr must be a number"); + return; + } + uint32_t addr = mjs_get_int32(mjs, addr_arg); + + mjs_val_t tx_buf_arg = mjs_arg(mjs, 1); + bool tx_buf_was_allocated = false; + uint8_t* tx_buf = NULL; + size_t tx_len = 0; + if(mjs_is_array(tx_buf_arg)) { + tx_len = mjs_array_length(mjs, tx_buf_arg); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + tx_buf = malloc(tx_len); + tx_buf_was_allocated = true; + for(size_t i = 0; i < tx_len; i++) { + mjs_val_t val = mjs_array_get(mjs, tx_buf_arg, i); + if(!mjs_is_number(val)) { + ret_bad_args(mjs, "Data array must contain only numbers"); + free(tx_buf); + return; + } + uint32_t byte_val = mjs_get_int32(mjs, val); + if(byte_val > 0xFF) { + ret_bad_args(mjs, "Data array values must be 0-255"); + free(tx_buf); + return; + } + tx_buf[i] = byte_val; + } + } else if(mjs_is_typed_array(tx_buf_arg)) { + mjs_val_t array_buf = tx_buf_arg; + if(mjs_is_data_view(tx_buf_arg)) { + array_buf = mjs_dataview_get_buf(mjs, tx_buf_arg); + } + tx_buf = (uint8_t*)mjs_array_buf_get_ptr(mjs, array_buf, &tx_len); + if(tx_len == 0) { + ret_bad_args(mjs, "Data array must not be empty"); + return; + } + } else { + ret_bad_args(mjs, "Data must be an array, arraybuf or dataview"); + return; + } + + mjs_val_t rx_len_arg = mjs_arg(mjs, 2); + if(!mjs_is_number(rx_len_arg)) { + ret_bad_args(mjs, "Length must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + return; + } + size_t rx_len = mjs_get_int32(mjs, rx_len_arg); + if(rx_len == 0) { + ret_bad_args(mjs, "Length must not zero"); + if(tx_buf_was_allocated) free(tx_buf); + return; + } + uint8_t* rx_buf = malloc(rx_len); + + uint32_t timeout = 1; + if(mjs_nargs(mjs) > 3) { // Timeout is optional argument + mjs_val_t timeout_arg = mjs_arg(mjs, 3); + if(!mjs_is_number(timeout_arg)) { + ret_bad_args(mjs, "Timeout must be a number"); + if(tx_buf_was_allocated) free(tx_buf); + free(rx_buf); + return; + } + timeout = mjs_get_int32(mjs, timeout_arg); + } + + furi_hal_i2c_acquire(&furi_hal_i2c_handle_external); + bool result = furi_hal_i2c_trx( + &furi_hal_i2c_handle_external, addr, tx_buf, tx_len, rx_buf, rx_len, timeout); + furi_hal_i2c_release(&furi_hal_i2c_handle_external); + + mjs_val_t ret = MJS_UNDEFINED; + if(result) { + ret = mjs_mk_array_buf(mjs, (char*)rx_buf, rx_len); + } + if(tx_buf_was_allocated) free(tx_buf); + free(rx_buf); + mjs_return(mjs, ret); +} + +static void* js_i2c_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) { + UNUSED(modules); + mjs_val_t i2c_obj = mjs_mk_object(mjs); + mjs_set(mjs, i2c_obj, "isDeviceReady", ~0, MJS_MK_FN(js_i2c_is_device_ready)); + mjs_set(mjs, i2c_obj, "write", ~0, MJS_MK_FN(js_i2c_write)); + mjs_set(mjs, i2c_obj, "read", ~0, MJS_MK_FN(js_i2c_read)); + mjs_set(mjs, i2c_obj, "writeRead", ~0, MJS_MK_FN(js_i2c_write_read)); + *object = i2c_obj; + + return (void*)1; +} + +static const JsModuleDescriptor js_i2c_desc = { + "i2c", + js_i2c_create, + NULL, + NULL, +}; + +static const FlipperAppPluginDescriptor i2c_plugin_descriptor = { + .appid = PLUGIN_APP_ID, + .ep_api_version = PLUGIN_API_VERSION, + .entry_point = &js_i2c_desc, +}; + +const FlipperAppPluginDescriptor* js_i2c_ep(void) { + return &i2c_plugin_descriptor; +} diff --git a/applications/system/js_app/types/i2c/index.d.ts b/applications/system/js_app/types/i2c/index.d.ts new file mode 100644 index 000000000..ea0a640cb --- /dev/null +++ b/applications/system/js_app/types/i2c/index.d.ts @@ -0,0 +1,31 @@ +/** + * @brief Check if there is an I2C device ready on the bus + * @param address The device address to check + * @param timeout Timeout in milliseconds + */ +export declare function isDeviceReady(address: number, timeout?: number): boolean; + +/** + * @brief Write data to I2C device and return success status + * @param address The device address to write to + * @param data The data to write to the device + * @param timeout Timeout in milliseconds + */ +export declare function write(address: number, data: number[] | ArrayBuffer, timeout?: number): boolean; + +/** + * @brief Read data from I2C device or return undefined on failure + * @param address The device address to read from + * @param length How many bytes to read + * @param timeout Timeout in milliseconds + */ +export declare function read(address: number, length: number, timeout?: number): ArrayBuffer | undefined; + +/** + * @brief Write data then read from I2C device or return undefined on failure + * @param address The device address to talk to + * @param writeData The data to write to the device + * @param readLength How many bytes to read + * @param timeout Timeout in milliseconds + */ +export declare function writeRead(address: number, writeData: number[] | ArrayBuffer, readLength: number, timeout?: number): ArrayBuffer | undefined;