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
kindis specified, filter all devices to only select from devices matching that kind (e.g. "retroarch") - If
uriis 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
forceparameter is set - The
kindoruriparameters 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:
SnirkFilesystem.get_fileSnirkFilesystem.put_fileSnirkFilesystem.remove_fileSnirkFilesystem.make_directorySnirkFilesystem.read_directory
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...
[]