Skip to content

Snirk Usage and Examples

This doc demonstrates examples for using the snirk library in an application.

The primary means of interaction is through an instance of a Snirk object, which manages interaction to a single SNI device (per instance).

Connecting to SNI devices with Snirk

Below code snippet instantiates a Snirk object:

from snirk import Snirk

snirk = Snirk()

Note

By default, this uses an SNI server found at localhost:8191. If SNI is running on a different host or port, that channel can be specified at instantiation.

AsyncIO Overview

Snirk uses asyncio for asynchronous communication to SNI devices (e.g. using async/await).

This means that most methods on a Snirk object return awaitables (e.g. coroutines), instead of actual results. For example, running .find_device returns a coroutine object:

>>> snirk.find_device()
<coroutine object Snirk.find_device at 0x74dc0c78a6b0>

This fits well with writing asynchronous applications, but for use in a REPL or synchronous applications (e.g. building CLI tools using Snirk), the asyncio.run method can be used to wait for results (or timeout).

Many of the below examples use asyncio.run in this way.

Listing all connected devices

Note

In the below examples, two SNI devices are connected (FxPak Pro and Retroarch). Your actual devices will differ depending on device, platform, and random assignment.

This object can then be used to list all connected devices (Snirk.list_devices):

>>> asyncio.run(snirk.list_devices())
[uri: "fxpakpro://./dev/ttyACM4"
displayName: "/dev/ttyACM4 (1209:5a22)"
kind: "fxpakpro"
capabilities: ReadMemory
capabilities: WriteMemory
capabilities: ResetSystem
capabilities: ResetToMenu
capabilities: ExecuteASM
capabilities: FetchFields
capabilities: ReadDirectory
capabilities: MakeDirectory
capabilities: RemoveFile
capabilities: RenameFile
capabilities: PutFile
capabilities: GetFile
capabilities: BootFile
, uri: "ra://127.0.0.1:55355"
displayName: "RetroArch v1.16.0 (127.0.0.1:55355)"
kind: "retroarch"
capabilities: ReadMemory
capabilities: WriteMemory
capabilities: ResetSystem
capabilities: PauseToggleEmulation
capabilities: FetchFields
defaultAddressSpace: SnesABus
]

In the above, details for each device are shown, including the uri, kind (e.g. "fxpakpro", "retroarch"), and certain capabilities.

Selecting a device

The Snirk.find_device method can be used to find and select a SNI device. The selected device is cached and used for all subsequent operations with the Snirk object.

When multiple devices are connected, the selected device is determined by below:

  • If kind is specified, filter all devices to only select from devices matching that kind (e.g. "retroarch")
  • If uri is specified, select the matching device (if it is detected)
  • Otherwise, choose the first matching device (which is not necessarily deterministic)

Extending our example above, where two devices (FxPak Pro and Retroarch) are connected, we specify using one device (the FxPak Pro) with kind:

>>> asyncio.run(snirk.find_device(kind="fxpakpro"))
uri: "fxpakpro://./dev/ttyACM4"
displayName: "/dev/ttyACM4 (1209:5a22)"
kind: "fxpakpro"
capabilities: ReadMemory
capabilities: WriteMemory
capabilities: ResetSystem
capabilities: ResetToMenu
capabilities: ExecuteASM
capabilities: FetchFields
capabilities: ReadDirectory
capabilities: MakeDirectory
capabilities: RemoveFile
capabilities: RenameFile
capabilities: PutFile
capabilities: GetFile
capabilities: BootFile

Subsequent calls to .find_device will use the value from the cache, unless:

  • The force parameter is set
  • The kind or uri parameters are used and do not match the currently-cached device

Additionally, the Snirk.device property can be used (it performs asyncio.run itself):

>>> snirk.device.uri
'fxpakpro://./dev/ttyACM4'

Note

The kind or uri cannot be specified on the .device property, so if a device has not already been cached from a previous call to .find_device then it will just use the first device found.

Reading and using memory data from SNI devices

AddressEnum and MemData

The AddressEnum and MemData classes are intended to allow easily translating memory data gRPC responses read over SNI from a game/ROM to named attributes for lookups.

In the below snippet, a (partial) mapping of address/size pairs from A Link to the Past Randomizer are mapped to names for easy lookup as an AddressEnum. We also create a MemData class which uses this new AddressEnum type to translate gRPC responses into a dict-like mapping.

from snirk.types import AddressEnum
from snirk.types import MemData


class AlttprAddresses(AddressEnum):
    # current MSU is $010B in WRAM (starts at 0xF50000)
    # https://github.com/KatDevsGames/z3randomizer/blob/master/msu.asm#L126
    CURRENT_MSU = (0xF5010B, 0x1)

    # from https://github.com/KrisDavie/DoorTracker/blob/main/DoorsTracker.py
    DUNGEON = (0xF5040C, 0x1)
    GAMEMODE = (0xF50010, 0x1)
    INDOORS = (0xF5001B, 0x1)
    LAMPCONE = (0xF50458, 0x1)


class AlttprMemData(MemData):
    memclass = AlttprAddresses

Reading a single memory address

The memory reading functions of Snirk are intended to be passed AddressEnum arguments and return gRPC responses which can be coerced into dict mappings for ease-of-use via MemData.

In the below snippet, we use Snirk.single_read with the classes we defined above to read a single memory address and then coerce into a dict:

response = asyncio.run(snirk.single_read(AlttprAddresses.GAMEMODE))
memdata = AlttprMemData(response)
>>> response
uri: "fxpakpro://./dev/ttyACM3"
response {
  requestAddress: 16056336
  deviceAddress: 16056336
  data: "\007"
}

>>> memdata
{<AlttprAddresses.GAMEMODE: (16056336, 1)>: 7}

Reading multiple memory addresses

Below, Snirk.multi_read is used to read multiple addresses in a single request:

responses = asyncio.run(snirk.multi_read(*AlttprAddresses))
memdata = AlttprMemData(responses)
>>> responses
uri: "fxpakpro://./dev/ttyACM3"
responses {
  requestAddress: 16056587
  deviceAddress: 16056587
  data: "\024"
}
responses {
  requestAddress: 16057356
  deviceAddress: 16057356
  data: "\000"
}
responses {
  requestAddress: 16056336
  deviceAddress: 16056336
  data: "\007"
}
responses {
  requestAddress: 16056347
  deviceAddress: 16056347
  data: "\001"
}
responses {
  requestAddress: 16057432
  deviceAddress: 16057432
  data: "\000"
}

>>> from pprint import pprint
>>> pprint(memdata)
{<AlttprAddresses.CURRENT_MSU: (16056587, 1)>: 20,
 <AlttprAddresses.GAMEMODE: (16056336, 1)>: 7,
 <AlttprAddresses.DUNGEON: (16057356, 1)>: 0,
 <AlttprAddresses.INDOORS: (16056347, 1)>: 1,
 <AlttprAddresses.LAMPCONE: (16057432, 1)>: 0}

Session context manager

In the above examples, we used asyncio.run to resolve awaitables in the python REPL.

Alternatively, in async applications, it can be convenient to use a Snirk object in a context manager (similar to sessions in aiohttp), using Snirk.session.

The below snippet demonstates this session usage, as well as iterating over multiple SNI devices. This snippet discovers all SNI devices and then reads some memory addresses from each:

import asyncio
import sys
from pprint import pprint
from snirk import Snirk
from snirk.api import SnirkError

import examples.snirk_types as st


async def main():
    # discover all SNI devices
    try:
        devices = await Snirk().list_devices()
    except SnirkError as exc:
        print(f"{exc}.\nIs SNI server running?")
        sys.exit(1)

    # handle some requests for each SNI device
    for device in devices:
        async with Snirk.session(uri=device.uri) as snirk:
            print(f"Device URI: {snirk.device.uri}")
            responses = await snirk.multi_read(*st.AlttprAddresses)

        memdata = st.AlttprMemData(responses)
        pprint(memdata)


if __name__ == "__main__":
    asyncio.run(main())

Example output:

Device URI: fxpakpro://./dev/ttyACM4
{<AlttprAddresses.DUNGEON: (16057356, 1)>: 85,
 <AlttprAddresses.CURRENT_MSU: (16056587, 1)>: 0,
 <AlttprAddresses.GAMEMODE: (16056336, 1)>: 85,
 <AlttprAddresses.INDOORS: (16056347, 1)>: 85,
 <AlttprAddresses.LAMPCONE: (16057432, 1)>: 85}
Device URI: ra://127.0.0.1:55355
{<AlttprAddresses.DUNGEON: (16057356, 1)>: 255,
 <AlttprAddresses.CURRENT_MSU: (16056587, 1)>: 0,
 <AlttprAddresses.GAMEMODE: (16056336, 1)>: 7,
 <AlttprAddresses.INDOORS: (16056347, 1)>: 1,
 <AlttprAddresses.LAMPCONE: (16057432, 1)>: 0}

Accessing device filesystems over SNI

Not all SNI devices necessarily have a device filesytem which is accessible. To work with device filesystems over SNI, a separate SnirkFilesystem class is provided (which inherits from Snirk).

Note

Not all SNI devices necessarily have a device filesytem which is accessible. The SnirkFilesystem methods will only work on devices with appropriate capabilities (which can be seen on the .device). On devices without the appropriate capabilities, a SnirkIncapableError exception will be raised.

A SnirkFilesystem instance provides several methods for interacting with device filesystems:

Example

The below example snippet shows connecting to a device filesystem and performing several operations:

import asyncio
from pathlib import Path
from snirk import SnirkFilesystem


async def main():
    contents = "Hello, SNI World."
    example_file = "/example/hello.txt"
    example_dir = str(Path(example_file).parent)

    async with SnirkFilesystem.session(kind="fxpakpro") as snirk:
        print(f"* Device URI: {snirk.device.uri}")

        print(f"* Making directory: {example_dir}...")
        await snirk.make_directory(example_dir)

        print(f"* Putting file: {example_file}...")
        await snirk.put_file(contents.encode(), example_file)

        print(f"* Reading directory: {example_dir}...")
        dirs = await snirk.read_directory(example_dir)
        print([en.name for en in dirs.entries if en.name not in [".", ".."]])

        print(f"* Reading file: {example_file}...")
        lines = await snirk.get_file(example_file)
        print(lines)

        print(f"* Removing file: {example_file}...")
        await snirk.remove_file(example_file)

        print(f"* Re-reading directory: {example_dir}...")
        dirs = await snirk.read_directory(example_dir)
        print([en.name for en in dirs.entries if en.name not in [".", ".."]])


if __name__ == "__main__":
    asyncio.run(main())

Example output:

* Device URI: fxpakpro://./dev/ttyACM0
* Making directory: /example...
* Putting file: /example/hello.txt...
* Reading directory: /example...
['hello.txt']
* Reading file: /example/hello.txt...
uri: "fxpakpro://./dev/ttyACM0"
path: "/example/hello.txt"
size: 17
data: "Hello, SNI World."

* Removing file: /example/hello.txt...
* Re-reading directory: /example...
[]