# GodMode9 Lua documentation

GodMode9 includes a Lua 5.4.8 implementation.

## Running scripts

There are four ways to run Lua scripts:

* Select it in the file browser and choose "`Execute Lua script`"
* Place it in `0:/gm9/luascripts`, then open the HOME menu, choose "`Lua scripts...`", then choose the script
* Place it in `data/luascripts`, then build GodMode9 from source
	* This takes precedence over `0:/gm9/luascripts`
* Place it at `data/autorun.lua`, then build GodMode9 from source with `SCRIPT_RUNNER=1` to automatically run it at launch

## Packages

Lua scripts can load custom modules. The default search path (defined as `package.path`) is:

```
0:/gm9/luapackages/?.lua;0:/gm9/luapackages/?/init.lua;V:/luapackages/?.lua;V:/luapackages/?/init.lua
```

For example, when a script calls `require("foobar")`, the package searcher will look for the module in this order:

* `0:/gm9/luapackages/foobar.lua`
* `0:/gm9/luapackages/foobar/init.lua`
* `V:/luapackages/foobar.lua`
* `V:/luapackages/foobar/init.lua`

## Comparison with GM9Script

These tables are provided to assist with converting from GM9Script to Lua.

### Commands

GM9 | Lua | Notes
-- | -- | --
goto | http://lua-users.org/wiki/GotoStatement |  
labelsel | ui.ask_selection |  
for | fs.find_all | Recursive searching does not work exactly the same, it will search for the filename in all directories
keychk | ui.check_key |  
echo | ui.echo |  
qr | ui.show_qr |  
ask | ui.ask |  
input | ui.ask_input |  
filesel | fs.ask_select_file |  
dirsel | fs.ask_select_dir |  
set | local var = value | More details on variables and scoping: https://www.lua.org/pil/4.2.html
strsplit | string.find and string.sub |  
strrep | string.gsub | https://www.lua.org/manual/5.4/manual.html#pdf-string.gsub
allow | fs.allow |  
cp | fs.copy |  
mv | fs.move |  
inject | fs.write_file |  
fill | fs.fill_file |  
fdummy | fs.make_dummy_file |  
rm | fs.remove |  
mkdir | fs.mkdir |  
mount | fs.img_mount |  
umount | fs.img_umount |  
find | fs.find |  
findnot | fs.find_not |  
fget | fs.read_file |  
fset | fs.write_file |  
sha | fs.hash_file OR fs.verify_with_sha_file | hash_file simply returns a hash, verify_with_sha_file compares it with a corresponding .sha file
shaget | fs.hash_file |  
dumptxt | fs.write_file | Use "end" for offset to append data
fixcmac | fs.fix_cmacs |  
verify | fs.verify |  
decrypt | title.decrypt |  
encrypt | title.encrypt |  
buildcia | title.build_cia |  
install | title.install |  
extrcode | title.extract_code |  
cmprcode | title.compress_code |  
sdump | fs.key_dump |  
applyips | target.apply_ips |  
applybps | target.apply_bps |  
applybpm | target.apply_bpm |  
textview | fs.show_file_text_viewer | fs.show_text_viewer can be used to show text from a variable
cartdump | fs.cart_dump |  
isdir | fs.is_dir |  
exist | fs.exists |  
boot | sys.boot |  
switchsd | fs.sd_switch |  
nextemu | sys.next_emu |  
reboot | sys.reboot |  
poweroff | sys.power_off |  
bkpt | bkpt |  

### PREVIEW_MODE variable

Unlike the `PREVIEW_MODE` GM9Script variable, this has been split into multiple functions.

Setting | Lua
-- | --
“quick" and “full" | (There is no alternative to view a Lua script as it’s running.)
“off" | ui.clear
text | ui.show_text
png file | ui.show_png
game icon | ui.show_game_info

### Other constants

GM9 | Lua | Notes
-- | -- | --
DATESTAMP | util.get_datestamp() | Formatted like “241202", equivalent to os.date("%y%m%d")
TIMESTAMP | util.get_timestamp() | Formatted like “010828", equivalent to os.date("%H%M%S")
SYSID0 | sys.sys_id0 |  
EMUID0 | sys.emu_id0 |  
EMUBASE | sys.emu_base |  
SERIAL | sys.serial |  
REGION | sys.region |  
SDSIZE | fs.stat_fs("0:/").total | int instead of string (use ui.format_bytes to format it)
SDFREE | fs.stat_fs("0:/").free | int instead of string (use ui.format_bytes to format it)
NANDSIZE | NANDSIZE | int instead of string (use ui.format_bytes to format it)
GM9OUT | GM9OUT |  
CURRDIR | CURRDIR | nil instead of “(null)" if it can’t be found
ONTYPE | CONSOLE_TYPE | “O3DS" or “N3DS"
RDTYPE | IS_DEVKIT | boolean instead of a string
HAX | HAX |  
GM9VER | GM9VER |  
GYROMODEL | GYROMODEL | int instead of string

## Comparisons with standard Lua

These original Lua 5.4 modules are fully available:
* [Basic functions](https://www.lua.org/manual/5.4/manual.html#6.1)
	* `print` will replace the top screen with an output log. Currently it does not implement some features such as line wrapping.
	* dofile and loadfile don't work yet.
* [coroutine](https://www.lua.org/manual/5.4/manual.html#6.2)
* [debug](https://www.lua.org/manual/5.4/manual.html#6.10)
* [math](https://www.lua.org/manual/5.4/manual.html#6.7)
* [string](https://www.lua.org/manual/5.4/manual.html#6.4)
* [table](https://www.lua.org/manual/5.4/manual.html#6.6)
* [utf8](https://www.lua.org/manual/5.4/manual.html#6.5)

These modules are partially available:
* [os](https://www.lua.org/manual/5.4/manual.html#6.9)
	* Only `os.clock`, `os.time`, `os.date`, `os.difftime`, and `os.remove`
* [io](https://www.lua.org/manual/5.4/manual.html#6.8)
	* Only `io.open`; for open files, all but `file:setvbuf`
	* This is a custom compatibility module that uses `fs` functions. If there are differences compared to the original `io` implementation, please report them as issues.

These modules work differently:
* [package](https://www.lua.org/manual/5.4/manual.html#6.3)
	* `package.cpath` and `package.loadlib` are nonfunctional due to GM9 having no ability to load dynamic libraries.

## Third-party libraries

rxi's [json.lua](https://github.com/rxi/json.lua) is pre-included. To use it, first load it:

```lua
local json = require("json")
```

---

## API reference

### Constants

#### GM9VER
The version such as `"v2.1.1-159-gff2cb913"`, the same string that is shown on the main screen.

#### GYROMODEL
The type of Gyro sensor present
* `1`: ITG-3270, used in O3DS and some O2DS
* `2`: ITG-1010, used in late model O2DS and N3DS
* `3`: Unknown name, used in late model N3DS
* `nil`: Unknown model

#### SCRIPT
Path to the executed script, such as `"0:/gm9/luascripts/myscript.lua"`.

#### CURRDIR
Directory of the executed script, such as `"0:/gm9/luascripts"`.

#### GM9OUT
The value `"0:/gm9/out"`.

#### HAX
> [!WARNING]
> This needs checking if it's accurate.
One of three values:
* "ntrboot" if started from an ntrboot cart
* "sighax" if booted directly from a FIRM partition
* Empty string otherwise

#### NANDSIZE
Total size of SysNAND in bytes.

#### CONSOLE_TYPE
The string `"O3DS"` or `"N3DS"`.

#### IS_DEVKIT
`true` if the console is a developer unit.

---

### `ui` module

> [!NOTE]
> This assumes the default build is used, where the bottom screen is the main screen. If GodMode9 is compiled with `SWITCH_SCREENS=1`, then every instance where something appears on the bottom screen will actually be on the top screen and vice versa.

#### ui.echo

* `void ui.echo(string text)`

Display text on the bottom screen and wait for the user to press A.

* **Arguments**
	* `text` - Text to show the user

#### ui.ask

* `bool ui.ask(string text)`

Prompt the user with a yes/no question.

* **Arguments**
	* `text` - Text to ask the user
* **Returns:** `true` if the user accepts

#### ui.ask_hex

* `int ui.ask_hex(string text, int initial, int n_digits)`

Ask the user to input a hex number.

* **Arguments**
	* `text` - Text to ask the user
	* `initial` - Starting value
	* `n_digits` - Amount of hex digits allowed
* **Returns:** the number the user entered, or `nil` if canceled

#### ui.ask_number

* `int ui.ask_number(string text, int initial)`

Ask the user to input a number.

* **Arguments**
	* `text` - Text to ask the user
	* `initial` - Starting value
* **Returns:** the number the user entered, or `nil` if canceled

#### ui.ask_text

* `string ui.ask_text(string prompt, string initial, int max_length)`

Ask the user to input text.

* **Arguments**
	* `prompt` - Text to ask the user
	* `initial` - Starting value
	* `max_length` - Maximum length of the string
* **Returns:** the text the user entered, or `nil` if canceled

#### ui.ask_selection

* `int ui.ask_selection(string prompt, table options)`

Ask the user to choose an option from a list. A maximum of 256 options are allowed.

* **Arguments**
	* `prompt` - Text to ask the user
	* `options` - Table of options
* **Returns:** index of selected option, or `nil` if canceled

#### ui.clear

* `void ui.clear()`

Clears the top screen.

#### ui.show_png

* `void ui.show_png(string path)`

Displays a PNG file on the top screen.

The image must not be larger than 400 pixels horizontal or 240 pixels vertical. If `SWITCH_SCREENS=1` is used, it must not be larger than 320 pixels horizontal.

* **Arguments**
	* `path` - Path to PNG file
* **Throws**
	* `"Could not read (file)"` - file does not exist or there was another read error
	* `"Invalid PNG file"` - file is not a valid PNG
	* `"PNG too large"` - too large horizontal or vertical, or an out-of-memory error

#### ui.show_text

* `void ui.show_text(string text)`

Displays text on the top screen.

* **Arguments**
	* `text` - Text to show the user

#### ui.show_game_info

* `void ui.show_game_info(string path)`

Shows game file info. Accepts any files that include an SMDH, a DS game file, or GBA file.

* **Arguments**
	* `path` - Path to game file (CIA, CCI/".3ds", SMDH, TMD, TIE (DSiWare export), Ticket, NDS, GBA)
* **Throws**
	* `"ShowGameFileIcon failed on <path>"` - failed to get game info from path

#### ui.show_qr

* `void ui.show_qr(string text, string data)`

Displays a QR code on the top screen, and a prompt on the bottom screen, and waits for the user to press A.

* **Arguments**
	* `text` - Text to show the user
	* `data` - Data to encode into the QR code
* **Throws**
	* `"could not allocate memory"` - out-of-memory error when attempting to generate the QR code

#### ui.show_text_viewer

* `void ui.show_text_viewer(string text)`

Display a scrollable text viewer.

* **Arguments**
	* `text` - Text to display
* **Throws**
	* `"text validation failed"` - given string contains invalid characters
	* `"failed to run MemTextViewer"` - internal memory viewer error

#### ui.show_file_text_viewer

* `void ui.show_file_text_viewer(string path)`

Display a scrollable text viewer from a text file.

* **Arguments**
	* `path` - Path to text file
* **Throws**
	* `"could not allocate memory"` - out-of-memory error when attempting to create the text buffer
	* `"text validation failed"` - text file contains invalid characters
	* `"failed to run MemTextViewer"` - internal memory viewer error

#### ui.format_bytes

* `string ui.format_bytes(int bytes[, table opts {bool use_locale}])`

Format a number with `Byte`, `kB`, `MB`, or `GB`. By default this will always use English style formatting. Enable `use_locale` to format in the user's selected language.

* **Arguments**
	* `bytes` - Size to format
	* `opts` (optional) - Option flags
		* `use_locale` - Format in the user's selected locale
* **Returns:** formatted string

#### ui.check_key

* `bool ui.check_key(string key)`

Checks if the user is holding down a key.

* **Arguments**
	* `key` - A button string: `"A"`, `"B"`, `"SELECT"`, `"START"`, `"RIGHT"`, `"LEFT"`, `"UP"`, `"DOWN"`, `"R"`, `"L"`, `"X"`, `"Y"`
* **Returns:** `true` if currently held, `false` if not

---

### `fs` module

#### fs.move

* `void fs.move(string src, string dst[, table opts {bool no_cancel, bool silent, bool overwrite, bool skip_all}])`

Moves or renames a file or directory.

* **Arguments**
	* `src` - Item to move
	* `dst` - Destination name
	* `opts` (optional) - Option flags
		* `no_cancel` - Don’t allow user to cancel
		* `silent` - Don’t show progress
		* `overwrite` - Overwrite files
		* `skip_all` - Skip existing files
* **Throws**
	* `"writing not allowed: <path>"` - user denied permission
	* `"destination already exists on <src> -> <dst> and {overwrite=true} was not used"` - attempted to move an item over an existing one without using `overwrite`
	* `"PathMoveCopy failed on <src> -> <dst>"` - error when moving, or user canceled

#### fs.remove

* `void fs.remove(string path[, table opts {bool recursive}])`

Delete a file or directory.

* **Arguments**
	* `path` - Path to delete
	* `opts` (optional) - Option flags
		* `recursive` - Remove directories recursively
* **Throws**
	* `"writing not allowed: <path>"` - user denied permission
	* `"requested directory remove without {recursive=true} on <path>"` - attempted to delete a directory without using `recursive`
	* `"PathDelete failed on %s"` - error when deleting

#### fs.copy

* `void fs.copy(string src, string dst[, table opts {bool calc_sha, bool sha1, bool no_cancel, bool silent, bool overwrite, bool skip_all, bool append, bool recursive}])`

Copy a file or directory.

* **Arguments**
	* `src` - Item to copy
	* `dst` - Destination name
	* `opts` (optional) - Option flags
		* `calc_sha` - Write `.sha` files containing a SHA-256 (default) or SHA-1 hash
		* `sha1` - Use SHA-1
		* `no_cancel` - Don’t allow user to cancel
		* `silent` - Don’t show progress
		* `overwrite` - Overwrite files
		* `skip_all` - Skip existing files
		* `append` - Append to the end of existing files instead of overwriting
		* `recursive` - Copy directories recursively
* **Throws**
	* `"writing not allowed: <path>"` - user denied permission
	* `"requested directory copy without {recursive=true} on <src> -> <dst>"` - attempted to copy a directory without using `recursive`
	* `"destination already exists on <src> -> <dst> and {overwrite=true} was not used"` - attempted to copy an item over an existing one without using `overwrite`
	* `"PathMoveCopy failed on <src> -> <dst>"` - error when copying, or user canceled

#### fs.mkdir

* `void fs.mkdir(string path)`

Create a directory. This creates intermediate directories as required, so `fs.mkdir("a/b/c")` would create `a`, then `b`, then `c`.

* **Arguments**
	* `path` - Directory to create
* **Throws**
	* `"writing not allowed: <path>"` - user denied permission
	* `"could not mkdir (<path>)"` - error when creating directories

#### fs.stat

* `table fs.stat(string path)`

Get information about a file or directory. The result is a stat table with these keys:

* **Arguments**
	* `path` - Directory to stat
* **Returns:** A stat table with keys:
	* `name` (string)
	* `type` (string) - `"dir"` or `"file"`
	* `size` (number)
	* `read_only` (bool)
* **Throws**
	* `"could not stat <path> (##)"` - error when attempting to stat item, with FatFs error number

#### fs.list_dir

* `table fs.list_dir(string path)`

Get the contents of a directory. The result is a list of stat tables with these keys:
* `name` (string)
* `type` (string) - `"dir"` or `"file"`
* `size` (number)
* `read_only` (bool)

* **Arguments**
	* `path` - Directory to list
* **Returns:** A list of stat tables, each with keys:
	* `name` (string)
	* `type` (string) - `"dir"` or `"file"`
	* `size` (number)
	* `read_only` (bool)
* **Throws**
	* `"could not opendir <path> (##)"` - error when attempting to open directory, with FatFs error number
	* `"could not readdir <path> (##)"` - error when attempting to read directory, with FatFs error number

#### fs.stat_fs

* `table fs.stat_fs(string path)`

Get information about a filesystem.

> [!NOTE]
> This function can take several seconds before it returns an answer.

* **Arguments**
	* `path` - Filesystem to stat
* **Returns:** A stat table with keys:
	* `free` (number)
	* `total` (number)
	* `used` (number)

#### fs.dir_info

* `table fs.dir_info(string path)`

Get information about a directory.

> [!NOTE]
> This function can take several seconds before it returns an answer.

* **Arguments**
	* `path` - Directory to check
* **Returns:** An info table with keys:
	* `size` (number)
	* `dirs` (number)
	* `files` (number)
* **Throws**
	* `"error when running DirInfo"` - error when scanning directory

#### fs.ask_select_file

* `string fs.ask_select_file(string prompt, string path[, bool opts {bool include_dirs, bool explorer}])`

Prompt the user to select a file based on a pattern. Accepts a wildcard pattern like `"0:/gm9/in/*_ctrtransfer_n3ds.bin"`.

* **Arguments**
	* `prompt` - Text to ask the user
	* `path` - Wildcard pattern to search for
	* `opts` (optional) - Option flags
		* `include_dirs` - Include directories in selection
		* `explorer` - Use file explorer, including navigating subdirectories
* **Returns:** path selected, or `nil` if user canceled
* **Throws**
	* `"forbidden drive"` - attempted search on `Z:`
	* `"invalid path"` - could not find `/` in path

#### fs.ask_select_dir

* `string fs.ask_select_dir(string prompt, string path[, bool opts {bool explorer}])`

Prompt the user to select a directory.

* **Arguments**
	* `prompt` - Text to ask the user
	* `path` - Directory to search
	* `opts` (optional) - Option flags
		* `explorer` - Use file explorer, including navigating subdirectories
* **Returns:** path selected, or `nil` if user canceled
* **Throws**
	* `"forbidden drive"` - attempted search on `Z:`

#### fs.find

* `string fs.find(string pattern[, bool opts {bool first}])`

Searches for a file based on a wildcard pattern. Returns the last result, unless `first` is specified, or `nil` if nothing was found.

Pattern can use `?` for search values, for example `00017?02` will match `00017002`, `00017102`, etc. Wildcards are also accepted.

* **Arguments**
	* `pattern` - Pattern to search for
	* `opts` (optional) - Option flags
		* `first` - Return first result instead of last
* **Returns:** found file, or `nil` if nothing is found
* **Throws**
	* `"failed to find <path> (##)"` - error when attempting to find path, with FatFs error number

#### fs.find_not

* `string fs.find(string pattern)`

Searches for a free filename based on a pattern.

Pattern can use `?` for search values, for example `nand_??.bin` will check to see if `nand_00.bin` exists. If it doesn't, it returns this string. Otherwise, it checks if `nand_01.bin` exists and keeps going until an unused filename can be found.

* **Arguments**
	* `pattern` - Pattern to search for
* **Returns:** found file
* **Throws**
	* `"failed to find <path> (##)"` - error when attempting to find path, with FatFs error number

#### fs.find_all

* `table fs.find_all(string dir, string pattern[, table opts {bool recursive}])`

Search for all files that match a pattern.
* **Arguments**
	* `dir` - Directory to search
    * `pattern` - Filename pattern
	* `opts` (optional) - Option flags
		* `recursive` - Remove directories recursively
* **Returns:** table of found files
* **Throws**
	* `"could not open directory"` - failed to open directory

#### fs.allow

* `bool fs.allow(string path[, table flags {bool ask_all}])`

Check for and request permission to write to a sensitive path.

* **Arguments**
	* `path` - Path to request permission for
	* `opts` (optional) - Option flags
		* `ask_all` - Request to write to all files in directory
* **Returns:** `true` if granted, `false` if user declines

#### fs.img_mount

* `void fs.img_mount(string path)`

Mount an image file. Can be anything mountable through the file browser.

* **Arguments**
	* `path` - Path to image file
* **Throws**
	* `"failed to mount <path>"` - not a valid image file

#### fs.img_umount

* `void fs.img_umount()`

Unmount the currently mounted image file.

#### fs.get_img_mount

* `string fs.get_img_mount()`

Get the currently mounted image file.

* **Returns:** path to file, or `nil` if none is mounted

#### fs.hash_file

* `string fs.hash_file(string path, int offset, int size[, table opts {bool sha1}])`

Calculate the hash for a file. Uses SHA-256 unless `sha1` is specified. To hash an entire file, `size` should be `0`.

> [!TIP]
> * Use `fs.verify_with_sha_file` to compare with a corresponding `.sha` file.
> * Use `util.bytes_to_hex` to convert the result to printable hex characters.

> [!NOTE]
> Using an offset that is not `0`, with a size of `0` (to hash to end of file), is currently undefined behavior. In the future this should work properly.

* **Arguments**
	* `path` - File to hash
	* `offset` - Data offset
	* `size` - Amount of data to hash, or `0` to hash to end of file
	* `opts` (optional) - Option flags
		* `sha1` - Use SHA-1
* **Returns:** SHA-256 or SHA-1 hash as byte string
* **Throws**
	* `"failed to stat <path>"` - could not stat file to get size
	* `"FileGetSha failed on <path>"` - could not read file or user canceled

#### fs.hash_data

* `string fs.hash_data(string data[, table opts {bool sha1}])`

Calculate the hash for some data. Uses SHA-256 unless `sha1` is specified.

> [!TIP]
> * Use `util.bytes_to_hex` to convert the result to printable hex characters.

* **Arguments**
	* `data` - Data to hash
	* `opts` (optional) - Option flags
		* `sha1` - Use SHA-1
* **Returns:** SHA-256 or SHA-1 hash as byte string

#### fs.verify

* `bool fs.verify(string path[, table opts {bool sig_check}])`

Verify the integrity of a file.

> [!NOTE]
> This is for files that have their own hashes built-in. For verifying against a corresponding `.sha` file, use `fs.verify_with_sha_file`.

* **Arguments**
	* `path` - File to verify
	* `opts` (optional) - Option flags
		* `sig_check` - Verify NCSD/NCCH signature
* **Returns:** `true` if successful, `false` if failed or not verifiable

#### fs.verify_with_sha_file

* `bool fs.verify_with_sha_file(string path)`

Calculate the hash of a file and compare it with a corresponding `.sha` file.

> [!IMPORTANT]
> This currently assumes SHA-256. In the future this may automatically use SHA-1 when appropriate, based on the `.sha` file size.

* **Argumens**
	* `path` - File to hash
* **Returns:** `true` if successful, `false` if failed, `nil` if `.sha` file could not be read
* **Throws**
	* `"failed to stat <path>"` - could not stat file to get size
	* `"FileGetSha failed on <path>"` - could not read file or user canceled

#### fs.exists

* `bool fs.exists(string path)`

Check if an item exists.

* **Arguments**
	* `path` - Path to file or directory
* **Returns:** `true` if exists, `false` otherwise

#### fs.is_dir

* `bool fs.is_dir(string path)`

Check if an item exists, and is a directory.

* **Arguments**
	* `path` - Path to directory
* **Returns:** `true` if exists and is a directory, `false` otherwise

#### fs.is_file

* `bool fs.is_file(string path)`

Check if an item exists, and is a file.

* **Arguments**
	* `path` - Path to file
* **Returns:** `true` if exists and is a file, `false` otherwise

#### fs.sd_is_mounted

* `bool fs.sd_is_mounted()`

Check if the SD card is mounted.

* **Returns:** `true` if SD card is mounted

#### fs.sd_switch

* `void fs.sd_switch([string message])`

Prompt the user to remove and insert an SD card.

* **Arguments**
	* `message` (optional) - Text to prompt the user, defaults to `"Please switch the SD card now."`
* **Throws**
	* `"user canceled"` - user canceled the switch

#### fs.fix_cmacs

* `void fs.fix_cmacs(string path)`

Fix CMACs for a directory.

* **Arguments**
	* `path` - Path to recursively fix CMACs for
* **Throws**
	* `fixcmac failed` - user denied permission, or fixing failed

#### fs.create_dbs

* `void fs.create_dbs(string destdrv[, table opts {bool silent, bool overwrite}])`

(Re)create potentially missing .db files (except certs.db) for the given drive.

Supported drives are:
- SysNAND/EmuNAND CTRNAND (`1:`/`4:`): `import.db`, `ticket.db`, `title.db`, `tmp_i.db`, `tmp_t.db`
- SysNAND/EmuNAND SD (`A:`/`B:`): `import.db`, `title.db`

* **Arguments**
	* `destdrv` - Drive to (re)create .db files for.
* `opts` (optional) - Option flags
	* `overwrite` - Overwrite existing .db files (use with caution!)
	* `silent` - Do not show any messages during the process (e.g. which specific file couldn't be created, number of created/already existing files, etc.)
* **Throws**
	* `createdbs failed` - user denied permission, or .db file(s) creation failed

#### fs.read_file

* `string fs.read_file(string path, int offset, int/string size)`

Read data from a file.

* **Arguments**
	* `path` - File to read
	* `offset` - Data offset
	* `size` - Amount of data to read
* **Returns:** string of data
* **Throws**
	* `"could not allocate memory to read file"` - out-of-memory error when attempting to create the data buffer
	* `"could not read <path> (##)"` - error when attempting to read file, with FatFs error number

#### fs.write_file

* `int fs.write_file(string path, int offset, string data)`

Write data to a file.

* **Arguments**
	* `path` - File to write
	* `offset` - Offset to write to, or the string `"end"` to write at the end of file
	* `data` - Data to write
* **Returns:** amount of bytes written
* **Throws**
	* `"writing not allowed: <path>"` - user denied permission
	* `"error writing <path> (##)"` - error when attempting to write file, with FatFs error number

#### fs.fill_file

* `void fs.fill_file(string path, int offset, int size, int byte)`

Fill a file with a specified byte.

* **Arguments**
	* `path` - File to write
	* `offset` - Offset to write to
	* `size` - Amount of data to write
	* `byte` - Number between `0x00` and `0xFF` (`0` and `255`) indicating the byte to write
	* `opts` (optional) - Option flags
		* `no_cancel` - Don’t allow user to cancel
* **Throws**
	* `"writing not allowed: <path>"` - user denied permission
	* `"byte is not between 0x00 and 0xFF (got: ##)"` - byte value is not a single byte
	* `"FileSetByte failed on <path>"` - writing failed or user canceled

#### fs.make_dummy_file

* `void fs.make_dummy_file(string path, int size)`

Create a dummy file.

> [!NOTE]
> The file will contain data from the unused parts of the filesystem. If you need to ensure it's clean, use `fs.fill_file`.

* **Arguments**
	* `path` - File to create
	* `size` - File size to set
* **Throws**
	* `"writing not allowed: <path>"` - user denied permission
	* `"FileCreateDummy failed on <path>"` - dummy creation failed

#### fs.truncate

* `void fs.truncate(string path, int size)`

Truncate a file to a specific size.

> [!IMPORTANT]
> Does not work for virtual filesystems.

* **Arguments**
	* `path` - File to create
	* `size` - File size to set
* **Throws**
	* `"writing not allowed: <path>"` - user denied permission
	* `"failed to open <path> (note: this only works on FAT filesystems, not virtual)"` - opening file failed, or a virtual filesystem was used
	* `"failed to seek on <path>"` - seeking file failed
	* `"failed to truncate on <path>"` - truncating file failed

#### fs.key_dump

* `void fs.key_dump(string file[, table opts {bool overwrite}])`

Dumps title keys or seeds. Taken from both SysNAND and EmuNAND. The resulting file is saved to `0:/gm9/out`.

* **Arguments**
	* `file` - One of three supported filenames: `seeddb.bin`, `encTitleKeys.bin`, `decTitleKeys.bin`
	* `opts` (optional) - Option flags
		* `overwrite` - Overwrite files
* **Throws**
	* `"building <file> failed"` - building failed or file already exists and `overwrite` was not used

#### fs.cart_dump

* `void fs.cart_dump(string path, int size[, table opts {bool encrypted}])`

Dump the raw data from the inserted game card. No modifications are made to the data. This means for example, Card2 games will not have the save area cleared.

* **Arguments**
	* `path` - File to write data to
	* `size` - Amount of data to read
	* `opts` (optional) - Option flags
		* `encrypted` - Dump game encrypted, only for DS/DSi games?
* **Throws**
	* `"out of memory"` - out-of-memory error when attempting to create the data buffer
	* `"cart init fail"` - card is not inserted or some other failure when attempting to initialize
	* `"cart dump failed or canceled"` - cart read failed or used canceled

---

### `title` module

#### title.decrypt, title.encrypt

* `void title.decrypt(string path)`
* `void title.decrypt(string path)`

Decrypt or encrypt a title or key database, done in-place.

* **Arguments**
	* `path` - Path to title or key database
* **Throws**
	* `"CryptAesKeyDb failed on <path>"` - could not crypt key database
	* `"CryptGameFile failed on <path>"` - could not crypt title

#### title.install

* `void title.install(string path[, table opts {bool to_emunand}])`

Install a title.

* **Arguments**
	* `path` - File to install
	* `opts` (optional) - Option flags
		* `to_emunand` - Install to EmuNAND
* **Throws**
	* `"InstallGameFile failed on <path>"` - install failed or user canceled

#### title.build_cia

* `void title.build_cia(string path[, table opts {bool legit}])`

Build title as a CIA. Resulting file is put in `0:/gm9/out`.

* **Arguments**
	* `path` - File to build as CIA
	* `opts` (optional) - Option flags
		* `legit` - Build as legit CIA if possible
* **Throws**
	* `"BuildCiaFromGameFile failed on <path>"` - build failed or user canceled

#### title.extract_code

* `void title.extract_code(string src, string dst)`

Extracts the `.code` from a title and decompresses it.

* **Arguments**
	* `src` - File containing `.code`
	* `dst` - Destination to write decompressed `.code`
* **Throws**
	* `"writing not allowed: <path>"` - user denied permission
	* `"<path> does not have code"` - invalid file type
	* `"failed to extract code from <path>"` - extraction failed

#### title.compress_code

* `void title.compress_code(string src, string dst)`

Compress a `.code` file.

* **Arguments**
	* `src` - Extracted `.code`, like from `title.extract_code`
	* `dst` - Destination to write compressed `.code`
* **Throws**
	* `"writing not allowed: <path>"` - user denied permission
	* `"failed to compress code from <path>"` - compression failed

#### title.apply_ips, title.apply_bps, title.apply_bpm

* `void title.apply_ips(string patch, string src, string target)`
* `void title.apply_bps(string patch, string src, string target)`
* `void title.apply_bpm(string patch, string src, string target)`

Apply an IPS, BPS, or BPM patch.

* **Arguments**
	* `patch` - IPS, BPS, or BPM patch file
	* `src` - File to patch
	* `target` - Destination to write patched file to
* **Throws**
	* `"ApplyIPSPatch failed"` - failed to apply IPS patch
	* `"ApplyBPSPatch failed"` - failed to apply BPS patch
	* `"ApplyBPMPatch failed"` - failed to apply BPM patch

---

### `sys` module

#### sys.boot

* `void sys.boot(string path)`

Boot a FIRM file.

* **Arguments**
	* `path` - FIRM file
* **Throws**
	* `"out of memory"` - out-of-memory error when attempting to create the data buffer
	* `"not a bootable firm"` - file cannot be booted

#### sys.reboot

* `void sys.reboot()`

Reboots the console.

#### sys.power_off

* `void sys.power_off()`

Powers off the console.

#### sys.region

* `string sys.region`

System region, based on SysNAND's `SecureInfo_[AB]` file. Not affected by `SecureInfo_C`.

Possible values: `"JPN"`, `"USA"`, `"EUR"`, `"AUS"`, `"CHN"`, `"KOR"`, `"TWN"`, `nil` (if it does not exist, or the region byte is invalid)

#### sys.serial

* `string sys.serial`

Serial number, based on SysNAND's `SecureInfo_[AB]` file. Not affected by `SecureInfo_C`.

Can be `nil` if the file does not exist.

#### sys.secureinfo_letter

* `string sys.secureinfo_letter`

The letter at the end of the console's `SecureInfo_[AB]` file.

Possible values: `"A"`, `"B"`, `nil` (if it does not exist)

#### sys.sys_id0

* `string sys.sys_id0`

ID0 of SysNAND as a printable hex string.

Can be `nil` if the file does not exist.

#### sys.emu_id0

* `string sys.emu_id0`

ID0 of EmuNAND as a printable hex string.

Can be `nil` if the file does not exist.

#### sys.emu_base

* `int sys.emu_base`

Current EmuNAND base.

#### sys.refresh_info

* `void sys.refresh_info()`

Refresh the following variables:

* `sys.region`
* `sys.serial`
* `sys.secureinfo_letter`
* `sys.sys_id0`
* `sys.emu_id0`
* `sys.emu_base`

This function is automatically called at the beginning of Lua's execution, but errors are caught to prevent a premature exit. This could leave some of the variables at `nil`.

* **Throws**
	* `"could not read SecureInfo"` - `SecureInfo_[AB]` is missing
	* `"SecureInfo region byte is invalid"` - `SecureInfo_[AB]` is not between 0 and 6 inclusive

#### sys.next_emu

* `void sys.next_emu()`

Switch to the next available EmuNAND.

This will automatically call `sys.refresh_info`.

#### sys.check_embedded_backup

* `bool sys.check_embedded_backup()`

Check if `essential.exefs` is embedded into SysNAND, and prompts the user if it isn't. This is the same as the check that is done at boot for GodMode9, but this can be called in a build compiled with `SCRIPT_RUNNER=1` for autorun scripts that require `essential.exefs` to exist.

* **Returns:** `true` if it exists or was created, `false` if user declines, `nil` if console fails genuine NCSD check (modified partition table)

#### sys.check_raw_rtc

* `bool sys.check_raw_rtc()`

Check if the Raw RTC is set correctly, and prompts the user if it isn't. This is the same as the check that is done at boot for GodMode9, but this can be called in a build compiled with `SCRIPT_RUNNER=1` for autorun scripts that require `essential.exefs` to exist.

* **Returns:** `true` if set, `false` if user declines

---

### `util` module

#### util.bytes_to_hex

* `string util.bytes_to_hex(string data)`

Convert a byte string to printable hex characters.

Example: `"test\xaa\xbb\xcc\xdd"` -> `"74657374aabbccdd"`

* **Arguments**
	* `data` - Data to convert
* **Returns:** Hex characters

#### util.hex_to_bytes

* `string util.hex_to_bytes(string hexstring)`

Convert hex characters to a byte string.

Example: `"74657374aabbccdd"` -> `"test\xaa\xbb\xcc\xdd"`

* **Arguments**
	* `hexstring` - Hex characters to convert
* **Returns:** Byte string
* **Throws**
	* `"bad argument #1 to 'char' (number expected, got nil)"` - invalid hex characters

#### util.get_datestamp

* `string util.get_datestamp()`

Returns a date stamp formatted like "241202" for 2024 December 02. Equivalent to `os.date("%y%m%d")`.

* **Returns:** Date stamp

#### util.get_timestamp

* `string util.get_timestamp()`

Returns a date stamp formatted like "010828" for 01:08:28. Equivalent to `os.date("%H%M%S")`.

* **Returns:** Time stamp

#### util.running_as_module

* `bool util.running_as_module()`

Determines if the currently executing script was directly run, or was imported by another script. Useful for scripts that want to be usable directly, while also importable.

> [!NOTE]
> This is not well tested.

* **Returns:** `true` if the current script was imported as a module

---

### `i2c` module

#### i2c.dev ####
A list of devices for use with `i2c.read` and `i2c.write`

Lua | Value
-- | --
POWER | 0
POWER | 1
CAMERA2 | 2
MCU | 3
CAMERA3 | 4
LCD1 | 5
LCD2 | 6
GYRO3 | 9
GYRO | 10
GYRO2 | 11
DEBUG_PAD | 12
IR | 13
EEPROM | 14
NFC | 15
QTM | 16
N3DS_HID | 17

#### i2c.mcu ####

`i2c.mcu` contains registers and bitmasks for registers

##### i2c.mcu.reg #####
Contains some registers in the MCU

Lua | Value | Notes
-- | -- | --
VERSION_HIGH | 0x0 | Major version of the MCU firmware.
VERSION_LOW | 0x1 | Minor version of the MCU firmware.
RESET_EVENTS | 0x2 | @ref mcu.reset_event_flags
VCOM_TOP | 0x3 | Flicker/VCOM value for the top screen.
VCOM_BOTTOM | 0x4 | Flicker/VCOM value for the bottom screen.
FIRMWARE_UPLOAD_0 | 0x5 | Firmware upload register.
FIRMWARE_UPLOAD_1 | 0x6 | Firmware upload register.
FIRMWARE_UPLOAD_2 | 0x7 | Firmware upload register.
RAW_3D_SLIDER_POSITION | 0x8 | Position of the 3D slider.
VOLUME_SLIDER_POSITION | 0x9 | Position of the volume slider.
BATTERY_PCB_TEMPERATURE | 0xA | Temperature of the battery, measured on a sensor on the PCB.
BATTERY_PERCENTAGE_INT | 0xB | Integer part of the battery percentage.
BATTERY_PERCENTAGE_FRAC | 0xC | Fractional part of the battery percentage.
BATTERY_VOLTAGE | 0xD | Voltage of the battery, in units of 20 mV.
HW_STATUS | 0xE | Hardware status bits
POWER_STATUS | 0xF | @ref mcu.power_status_flags
LEGACY_VERSION_HIGH | 0xF | (Old MCU_FIRM only) Major firmware version.
LEGACY_VERSION_LOW | 0x10 | (Old MCU_FIRM only) Minor firmware version.
LEGACY_FIRM_UPLOAD | 0x3B | (Old MCU_FIRM only) Firmware upload register.
RECEIVED_IRQS_1 | 0x10 | Bitmask of received IRQs. @ref mcu.interrupt_1
RECEIVED_IRQS_2 | 0x11 | Bitmask of received IRQs. @ref mcu.interrupt_2
RECEIVED_IRQS_3 | 0x12 | Bitmask of received IRQs. @ref mcu.interrupt_3
RECEIVED_IRQS_4 | 0x13 | Bitmask of received IRQs. @ref mcu.interrupt_4
UNUSED_RO_14 | 0x14 | Unused read-only register.
USERDATA_RAM_15 | 0x15 | Unused register, supports writing
USERDATA_RAM_16 | 0x16 | Unused register, supports writing
USERDATA_RAM_17 | 0x17 | Unused register, supports writing
IRQ_MASK_1 | 0x18 | Bitmask of enabled IRQs.  @ref mcu.interrupt_1
IRQ_MASK_2 | 0x19 | Bitmask of enabled IRQs.  @ref mcu.interrupt_2
IRQ_MASK_3 | 0x1A | Bitmask of enabled IRQs.  @ref mcu.interrupt_3
IRQ_MASK_4 | 0x1B | Bitmask of enabled IRQs.  @ref mcu.interrupt_4
USERDATA_RAM_1C | 0x1C | Unused register, supports writing
USERDATA_RAM_1D | 0x1D | Unused register, supports writing
USERDATA_RAM_1E | 0x1E | Unused register, supports writing
USERDATA_RAM_1F | 0x1F | Unused register, supports writing
PWR_CTL | 0x20 | @ref mcu.power_trigger
LCD_PWR_CTL | 0x22 | LCD Power control
MCU_RESET_CTL | 0x23 | Writing 'r' to this register resets the MCU. Stubbed on retail.
FORCE_SHUTDOWN_DELAY | 0x24 | The amount of time, in units of 0.125s, for which the power button needs to be held to trigger a hard shutdown.
VOLUME_UNK_25 | 0x25 | Unknown register. Used in mcu::SND
UNK_26 | 0x26 | Unknown register. Used in mcu::CDC
VOLUME_SLIDER_RAW | 0x27 | Raw value of the volume slider, in the range of 0-255
LED_BRIGHTNESS_STATE | 0x28 | Brightness of the status LEDs.
POWER_LED_STATE | 0x29 | @ref mcu.power_led_state
WLAN_LED_STATE | 0x2A | Controls the WiFi LED.
CAMERA_LED_STATE | 0x2B | Controls the camera LED (on models that have one).
LED_3D_STATE | 0x2C | Controls the 3D LED (on models that have one).
NOTIFICATION_LED_STATE | 0x2D | Controls the info (notification) LED.
NOTIFICATION_LED_CYCLE_STATE | 0x2E | Bit 0 is set if the info (notification) LED has started a new cycle of its pattern.
RTC_TIME_SECOND | 0x30 | RTC second.
RTC_TIME_MINUTE | 0x31 | RTC minute.
RTC_TIME_HOUR | 0x32 | RTC hour.
RTC_TIME_WEEKDAY | 0x33 | RTC day of the week.
RTC_TIME_DAY | 0x34 | RTC day of the month.
RTC_TIME_MONTH | 0x35 | RTC month.
RTC_TIME_YEAR | 0x36 | RTC year.
RTC_TIME_CORRECTION | 0x37 | RTC subsecond (RSUBC) correction value.
RTC_ALARM_MINUTE | 0x38 | RTC alarm minute.
RTC_ALARM_HOUR | 0x39 | RTC alarm hour.
RTC_ALARM_DAY | 0x3A | RTC alarm day.
RTC_ALARM_MONTH | 0x3B | RTC alarm month.
RTC_ALARM_YEAR | 0x3C | RTC alarm year.
TICK_COUNTER_LSB | 0x3D | MCU tick counter value (low byte).
TICK_COUNTER_MSB | 0x3E | MCU tick counter value (high byte).
UNK_3F | 0x3F | Unknown register
SENSOR_CONFIG | 0x40 | @ref mcu.sensor_config
ACCELEROMETER_MANUAL_REGID_R | 0x41 | Hardware register ID for use in manual accelerometer I²C reads.
ACCELEROMETER_MANUAL_REGID_W | 0x43 | Hardware reigster ID for use in manual accelerometer I²C writes.
ACCELEROMETER_MANUAL_IO | 0x44 | Data register for manual accelerometer reads/writes.
ACCELEROMETER_OUTPUT_X_LSB | 0x45 | Accelerometer X coordinate (low byte).
ACCELEROMETER_OUTPUT_X_MSB | 0x46 | Accelerometer X coordinate (high byte).
ACCELEROMETER_OUTPUT_Y_LSB | 0x47 | Accelerometer Y coordinate (low byte).
ACCELEROMETER_OUTPUT_Y_MSB | 0x48 | Accelerometer Y coordinate (high byte).
ACCELEROMETER_OUTPUT_Z_LSB | 0x49 | Accelerometer Z coordinate (low byte).
ACCELEROMETER_OUTPUT_Z_MSB | 0x4A | Accelerometer Z coordinate (high byte).
PEDOMETER_STEPS_LOWBYTE | 0x4B | Pedometer step count (low byte).
PEDOMETER_STEPS_MIDDLEBYTE | 0x4C | Pedometer step count (middle byte).
PEDOMETER_STEPS_HIGHBYTE | 0x4D | Pedometer step count (high byte).
PEDOMETER_CNT | 0x4E | @ref mcu.pedometer_control
PEDOMETER_STEP_DATA | 0x4F | Step history and time of last update.
PEDOMETER_WRAP_MINUTE | 0x50 | The minute within each RTC hour at which the step history should roll into the next hour.
PEDOMETER_WRAP_SECOND | 0x51 | The second within each RTC hour at which the step history should roll into the next hour.
VOLUME_CALIBRATION_MIN | 0x58 | Lower bound of sound volume.
VOLUME_CALIBRATION_MAX | 0x59 | Upper bound of sound volume.
STORAGE_AREA_OFFSET | 0x60 | Offset within the storage area to read/write to.
STORAGE_AREA | 0x61 | Input/output byte for write/read operations in the storage area.
INFO | 0x7F | System information register.

##### i2c.mcu.interrupt_1 #####
Bitmask for interrupts (1)

Lua | Bit | Notes
-- | -- | --
POWER_BUTTON_PRESS | 0 | The power button was pressed
POWER_BUTTON_HELD | 1 | The power button was held
HOME_BUTTON_PRESS | 2 | The HOME button was pressed
HOME_BUTTON_RELEASE | 3 | The HOME button was released
WLAN_SWITCH_TRIGGER | 4 | The WiFi switch was triggered
SHELL_CLOSE | 5 | The shell was closed (or sleep switch turned off)
SHELL_OPEN | 6 | The shell was opened (or sleep switch turned on)
FATAL_HW_ERROR | 7 | MCU watchdog reset occurred

##### i2c.mcu.interrupt_2 #####
Bitmask for interrupts (2)

Lua | Bit | Notes
-- | -- | --
AC_ADAPTER_REMOVED | 0 | The AC adapter was disconnected
AC_ADAPTER_PLUGGED_IN | 1 | The AC adapter was connected
RTC_ALARM | 2 | The RTC alarm time has been reached
ACCELEROMETER_I2C_MANUAL_IO | 3 | The manual accelerometer I²C read/write operation has completed
ACCELEROMETER_NEW_SAMPLE | 4 | A new accelerometer sample is available
CRITICAL_BATTERY | 5 | The battery is critically low
CHARGING_STOP | 6 | The battery stopped charging
CHARGING_START | 7 | The battery started charging


##### i2c.mcu.interrupt_3 #####
Bitmask for interrupts (3)

Lua | Bit | Notes
-- | -- | --
VOL_SLIDER | 6 | The position of the volume slider changed


##### i2c.mcu.interrupt_4 #####
Bitmask for interrupts (4)

Lua | Bit | Notes
-- | -- | --
LCD_OFF | 0 | The LCDs turned off.
LCD_ON | 1 | The LCDs turned on.
BOT_BACKLIGHT_OFF | 2 | The bottom screen backlight turned off.
BOT_BACKLIGHT_ON | 3 | The bottom screen backlight turned on.
TOP_BACKLIGHT_OFF | 4 | The top screen backlight turned off.
TOP_BACKLIGHT_ON | 5 | The top screen backlight turned on.

##### i2c.mcu.reset_event_flags #####
Bitmasks for the reset event flags
Lua | Bit | Notes
-- | -- | --
RTC_TIME_LOST | 0 | RTC time was lost (as is the case when the battery is removed).
WATCHDOG_RESET | 1 | MCU Watchdog reset occurred.

##### i2c.mcu.power_status_flags #####
Bitmasks for the power status
Lua | Bit | Notes
-- | -- | --
SHELL_OPEN | 1 | Set if the shell is open.
ADAPTER_CONNECTED | 3 | Set if the AC adapter is connected.
CHARGING | 4 | Set if the battery is charging.
BOTTOM_BL_ON | 5 | Set if the bottom backlight is on.
TOP_BL_ON | 6 | Set if the top backlight is on.
LCD_ON | 7 | Set if the LCDs are on.

##### i2c.mcu.pedometer_control #####
Bitmasks for the pedometer control
Lua | Bit | Notes
-- | -- | --
CLEAR | 0 | Clear the step history.
STEPS_FULL | 4 | Set when the step history is full.

##### i2c.mcu.power_trigger #####
Bitmasks for the power signal triggers
Lua | Bit | Notes
-- | -- | --
SHUTDOWN | 0 | Turn off the system.
RESET | 1 | Reset the MCU.
REBOOT | 2 | Reboot the system.
LGY_SHUTDOWN | 3 | Turn off the system. (Used by LgyBg)
SLEEP | 4 | Signal to enter sleep mode.
OLDMCU_BL_OFF | 4 | (Old MCU_FIRM only) turn the backlights off.
OLDMCU_BL_ON | 5 | (Old MCU_FIRM only) turn the backlights on.
OLDMCU_LCD_OFF | 6 | (Old MCU_FIRM only) turn the LCDs off.
OLDMCU_LCD_ON | 7 | (Old MCU_FIRM only) turn the LCDs on.

##### i2c.mcu.power_led_state #####
Lua | Value | Notes
-- | -- | --
NORMAL | 0 | Fade power LED to blue, while checking for battery percentage.
FADE_BLUE | 1 | Fade power LED to blue.
SLEEP_MODE | 2 | The power LED pulses blue slowly, as it does in sleep mode.
OFF | 3 | Power LED fades off.
RED | 4 | Power LED instantaneously turns red.
BLUE | 5 | Power LED instantaneously turns blue.
BLINK_RED | 6 | Power LED and info (notification) LED blink red, as they do when the battery is critically low.

##### i2c.mcu.wifi_mode #####
WiFi mode register values
Lua | Value | Notes
-- | -- | --
CTR | 0 | 3DS WiFi mode.
MP | 1 | DS[i] WiFi mode ("MP").

##### i2c.mcu.sensor_config #####
Sensor configuration register values
Lua | Bit | Notes
-- | -- | --
ACCELEROMETER_ENABLE | 0 | If set, the accelerometer is enabled.
PEDOMETER_ENABLE | 1 | If set, the pedometer is enabled.

##### i2c.mcu.accelerometer_scale #####
Accelerometer scale settings
Lua | Value | Notes
-- | -- | --
SCALE_2G | 0x0 | -2G to 2G
SCALE_4G | 0x1 | -4G to 4G
SCALE_8G | 0x3 | -8G to 8G


#### i2c.read

* `table i2c.read(int device_id, int register_address, int length)`

Read data from an I²C device register. Returns the data as a table of integers (0-255) for direct math operations.

* **Arguments**
    * `device_id` - I²C device ID (0-17).
    * `register_address` - Register address to read from (0-255)
    * `length` - Number of bytes to read (1-64)
* **Returns:** Table of byte values as integers
* **Throws**
    * `"Invalid device ID: ## (must be 0-17)"` - device ID is outside valid range
    * `"Invalid register address: ## (must be 0-255)"` - register address is outside valid range
    * `"Invalid length: ## (must be 1-64)"` - length is outside valid range
    * `"I2C read failed"` - hardware I²C operation failed

Example:
```lua
-- Read voltage from MCU device
local voltage_raw = i2c.read(i2c.dev.MCU, i2c.mcu.reg.BATTERY_VOLTAGE, 1)
local voltage = voltage_raw[1] * 5 / 256.0
print("System voltage:", voltage, "V")

-- Read Power status
local power_state = i2c.read(i2c.dev.MCU, i2c.mcu.reg.POWER_STATUS, 1)
if power_state[1] & i2c.mcu.power_status_flags.SHELL_OPEN ~= 0 then
  print("Shell is open")
end
if power_state[1] & i2c.mcu.power_status_flags.CHARGING ~= 0 then
  print("Charging")
end
```

#### i2c.write

* `void i2c.write(int device_id, int register_address, table data)`

> [!WARNING]
> Writing data to random registers can lead to bricking!.

Write data to an I²C device register. Requires memory write permissions.
Not all registers are supported for writing.

* **Allowed registers (MCU)**
	* `mcu.reg.USERDATA_RAM_` - Registers 0x15-0x17 and 0x1C-0x1F
	* `mcu.reg.LED_BRIGHTNESS_STATE` - Brightness of the status LEDs.
	* `mcu.reg.POWER_LED_STATE` - Sets the state of the power LED
	* `mcu.reg.WLAN_LED_STATE` - Sets the state of the wifi LED
	* `mcu.reg.CAMERA_LED_STATE` - Sets the state of the camera LED
	* `mcu.reg.LED_3D_STATE` - Sets the state of the 3D LED
	* `mcu.reg.NOTIFICATION_LED_STATE` - Sets the state of the info (notification) LED
* **Arguments**
    * `device_id` - I²C device ID (0-17).
    * `register_address` - Register address to write to (0-255)
    * `data` - Table of byte values to write (each value must be 0-255)
* **Throws**
    * `"Invalid device ID: ## (must be 0-17)"` - device ID is outside valid range
    * `"Invalid register address: ## (must be 0-255)"` - register address is outside valid range
	* `"Write to device ##, register ## is not allowed"` - The device/register is not on the allowed write list
    * `"Third parameter must be a table of byte values"` - data parameter is not a table
    * `"Invalid data length: ## (must be 1-64)"` - data table length is outside valid range
    * `"Table element ## is not an integer"` - data table contains non-integer values
    * `"Table element ## is out of range: ## (must be 0-255)"` - data table contains values outside byte range
    * `"I2C write failed"` - hardware I²C operation failed
    * Permission errors if memory write access is denied
