Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript: SPI implementation #272

Merged
merged 8 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
- Updater: New Yappy themed icon while updating (#253 by @the1anonlypr3 & @Kuronons & @nescap)
- JS:
- New `i2c` module (#259 by @jamisonderek)
- New `spi` module (#272 by @jamisonderek)
- BadKB:
- OFW: Add linux/gnome badusb demo files (by @thomasnemer)
- Add older qFlipper install demos for windows and macos (by @DXVVAY & @grugnoymeme)
Expand Down
8 changes: 8 additions & 0 deletions applications/system/js_app/application.fam
Original file line number Diff line number Diff line change
Expand Up @@ -216,3 +216,11 @@ App(
requires=["js_app"],
sources=["modules/js_i2c.c"],
)

App(
appid="js_spi",
apptype=FlipperAppType.PLUGIN,
entry_point="js_spi_ep",
requires=["js_app"],
sources=["modules/js_spi.c"],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Connect a w25q32 SPI device to the Flipper Zero.
// D1=pin 2 (MOSI), SLK=pin 5 (SCK), GND=pin 8 (GND), D0=pin 3 (MISO), CS=pin 4 (CS), VCC=pin 9 (3V3)
let spi = require("spi");

// Display textbox so user can scroll to see all output.
let eventLoop = require("event_loop");
let gui = require("gui");
let text = "SPI demo\n";
let textBox = require("gui/text_box").makeWith({
focus: "end",
font: "text",
text: text,
});

function addText(add) {
text += add;
textBox.set("text", text);
}

gui.viewDispatcher.switchTo(textBox);

// writeRead returns a buffer the same length as the input buffer.
// We send 6 bytes of data, starting with 0x90, which is the command to read the manufacturer ID.
// Can also use Uint8Array([0x90, 0x00, ...]) as write parameter
// Optional timeout parameter in ms. We set to 100ms.
let data_buf = spi.writeRead([0x90, 0x0, 0x0, 0x0, 0x0, 0x0], 100);
let data = Uint8Array(data_buf);
if (data.length === 6) {
if (data[4] === 0xEF) {
addText("Found Winbond device\n");
if (data[5] === 0x15) {
addText("Device ID: W25Q32\n");
} else {
addText("Unknown device ID: " + data[5].toString(16) + "\n");
}
} else if (data[4] === 0x0) {
addText("Be sure Winbond W25Q32 is connected to Flipper Zero SPI pins.\n");
} else {
addText("Unknown device. Manufacturer ID: " + data[4].toString(16) + "\n");
}
}

addText("\nReading JEDEC ID\n");

// Acquire the SPI bus. Multiple calls will happen with Chip Select (CS) held low.
spi.acquire();

// Send command (0x9F) to read JEDEC ID.
// Can also use Uint8Array([0x9F]) as write parameter
// Note: you can pass an optional timeout parameter in milliseconds.
spi.write([0x9F]);

// Request 3 bytes of data.
// Note: you can pass an optional timeout parameter in milliseconds.
data_buf = spi.read(3);

// Release the SPI bus as soon as we are done with the set of SPI commands.
spi.release();

data = Uint8Array(data_buf);
addText("JEDEC MF ID: " + data[0].toString(16) + "\n");
addText("JEDEC Memory Type: " + data[1].toString(16) + "\n");
addText("JEDEC Capacity ID: " + data[2].toString(16) + "\n");

if (data[0] === 0xEF) {
addText("Found Winbond device\n");
}
let capacity = data[1] << 8 | data[2];
if (capacity === 0x4016) {
addText("Device: W25Q32\n");
} else if (capacity === 0x4015) {
addText("Device: W25Q16\n");
} else if (capacity === 0x4014) {
addText("Device: W25Q80\n");
} else {
addText("Unknown device\n");
}

// Wait for user to close the app
eventLoop.subscribe(gui.viewDispatcher.navigation, function (_sub, _, eventLoop) {
eventLoop.stop();
}, eventLoop);

// This script has no interaction, only textbox, so event loop doesn't need to be running all the time
// We run it at the end to accept input for the back button press to quit
// But before that, user sees a textbox and pressing back has no effect
// This is fine because it allows simpler logic and the code above takes no time at all to run
eventLoop.run();
283 changes: 283 additions & 0 deletions applications/system/js_app/modules/js_spi.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
#include "../js_modules.h"
#include <furi_hal_spi.h>

typedef struct {
bool acquired_bus;
} JsSpiInst;

static JsSpiInst* get_this_ctx(struct mjs* mjs) {
mjs_val_t obj_inst = mjs_get(mjs, mjs_get_this(mjs), INST_PROP_NAME, ~0);
JsSpiInst* spi = mjs_get_ptr(mjs, obj_inst);
furi_assert(spi);
return spi;
}

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_spi_acquire(struct mjs* mjs) {
if(!check_arg_count_range(mjs, 0, 0)) return;
JsSpiInst* spi = get_this_ctx(mjs);
if(!spi->acquired_bus) {
furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
spi->acquired_bus = true;
}
mjs_return(mjs, MJS_UNDEFINED);
}

static void js_spi_release(struct mjs* mjs) {
if(!check_arg_count_range(mjs, 0, 0)) return;
JsSpiInst* spi = get_this_ctx(mjs);
if(spi->acquired_bus) {
furi_hal_spi_release(&furi_hal_spi_bus_handle_external);
spi->acquired_bus = false;
}
mjs_return(mjs, MJS_UNDEFINED);
}

static bool js_spi_is_acquired(struct mjs* mjs) {
JsSpiInst* spi = get_this_ctx(mjs);
return spi->acquired_bus;
}

static void js_spi_write(struct mjs* mjs) {
if(!check_arg_count_range(mjs, 1, 2)) return;

mjs_val_t tx_buf_arg = mjs_arg(mjs, 0);
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) > 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");
if(tx_buf_was_allocated) free(tx_buf);
return;
}
timeout = mjs_get_int32(mjs, timeout_arg);
}

if(!js_spi_is_acquired(mjs)) {
furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
}
bool result = furi_hal_spi_bus_tx(&furi_hal_spi_bus_handle_external, tx_buf, tx_len, timeout);
if(!js_spi_is_acquired(mjs)) {
furi_hal_spi_release(&furi_hal_spi_bus_handle_external);
}

if(tx_buf_was_allocated) free(tx_buf);
mjs_return(mjs, mjs_mk_boolean(mjs, result));
}

static void js_spi_read(struct mjs* mjs) {
if(!check_arg_count_range(mjs, 1, 2)) return;

mjs_val_t rx_len_arg = mjs_arg(mjs, 0);
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) > 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");
free(rx_buf);
return;
}
timeout = mjs_get_int32(mjs, timeout_arg);
}

if(!js_spi_is_acquired(mjs)) {
furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
}
bool result = furi_hal_spi_bus_rx(&furi_hal_spi_bus_handle_external, rx_buf, rx_len, timeout);
if(!js_spi_is_acquired(mjs)) {
furi_hal_spi_release(&furi_hal_spi_bus_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_spi_write_read(struct mjs* mjs) {
if(!check_arg_count_range(mjs, 1, 2)) return;

mjs_val_t tx_buf_arg = mjs_arg(mjs, 0);
bool tx_buf_was_allocated = false;
uint8_t* tx_buf = NULL;
size_t data_len = 0;
if(mjs_is_array(tx_buf_arg)) {
data_len = mjs_array_length(mjs, tx_buf_arg);
if(data_len == 0) {
ret_bad_args(mjs, "Data array must not be empty");
return;
}
tx_buf = malloc(data_len);
tx_buf_was_allocated = true;
for(size_t i = 0; i < data_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, &data_len);
if(data_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;
}

uint8_t* rx_buf = malloc(data_len); // RX and TX are same length for SPI writeRead.

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");
if(tx_buf_was_allocated) free(tx_buf);
free(rx_buf);
return;
}
timeout = mjs_get_int32(mjs, timeout_arg);
}

if(!js_spi_is_acquired(mjs)) {
furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
}
bool result =
furi_hal_spi_bus_trx(&furi_hal_spi_bus_handle_external, tx_buf, rx_buf, data_len, timeout);
if(!js_spi_is_acquired(mjs)) {
furi_hal_spi_release(&furi_hal_spi_bus_handle_external);
}

mjs_val_t ret = MJS_UNDEFINED;
if(result) {
ret = mjs_mk_array_buf(mjs, (char*)rx_buf, data_len);
}
if(tx_buf_was_allocated) free(tx_buf);
free(rx_buf);
mjs_return(mjs, ret);
}

static void* js_spi_create(struct mjs* mjs, mjs_val_t* object, JsModules* modules) {
UNUSED(modules);
JsSpiInst* spi = (JsSpiInst*)malloc(sizeof(JsSpiInst));
spi->acquired_bus = false;
mjs_val_t spi_obj = mjs_mk_object(mjs);
mjs_set(mjs, spi_obj, INST_PROP_NAME, ~0, mjs_mk_foreign(mjs, spi));
mjs_set(mjs, spi_obj, "acquire", ~0, MJS_MK_FN(js_spi_acquire));
mjs_set(mjs, spi_obj, "release", ~0, MJS_MK_FN(js_spi_release));
mjs_set(mjs, spi_obj, "write", ~0, MJS_MK_FN(js_spi_write));
mjs_set(mjs, spi_obj, "read", ~0, MJS_MK_FN(js_spi_read));
mjs_set(mjs, spi_obj, "writeRead", ~0, MJS_MK_FN(js_spi_write_read));
*object = spi_obj;

furi_hal_spi_bus_handle_init(&furi_hal_spi_bus_handle_external);
return (void*)spi;
}

static void js_spi_destroy(void* inst) {
JsSpiInst* spi = (JsSpiInst*)inst;
if(spi->acquired_bus) {
furi_hal_spi_release(&furi_hal_spi_bus_handle_external);
}
free(spi);
furi_hal_spi_bus_handle_deinit(&furi_hal_spi_bus_handle_external);
}

static const JsModuleDescriptor js_spi_desc = {
"spi",
js_spi_create,
js_spi_destroy,
NULL,
};

static const FlipperAppPluginDescriptor spi_plugin_descriptor = {
.appid = PLUGIN_APP_ID,
.ep_api_version = PLUGIN_API_VERSION,
.entry_point = &js_spi_desc,
};

const FlipperAppPluginDescriptor* js_spi_ep(void) {
return &spi_plugin_descriptor;
}
Loading
Loading