MegaWiFi 1.5
MegaWiFi API documentation
|
This repository contains the MegaWiFi API, to use the WiFi capabilities of the MegaWiFi cartridges.
The full API documentation can be found here.
You will need a complete Genesis/Megadrive toolchain. The sources use some C standard library calls, such as memcpy()
, strchr()
, etc. Thus your toolchain must include a C standard library implementation such as newlib. Alternatively you can use the version integrated into the awesome SGDK.
The MegaWiFi API consists of the following modules:
The mw-msg
module contains the message definitions for the different MegaWiFi commands and command replies. Fear not because usually you do not need to use this module, unless you are doing something pretty advanced not covered by the megawifi
module API.
The util
module contains general purpose functions and macros not fitting in the other modules, such as ip_validate()
to check if a string is a valid IP address, str_to_uint8()
to convert a string to an 8-bit number, etc. There is also a json
module wich includes jsmn
library along with some helper functions to parse JSON formatted strings. Please read jsmn
documentation to learn how its tokenizer works.
Previous MegaWiFi releases also included two more modules: mpool
(a quite limited dynamic memory allocator) and loop
(a loop functions and timers implementation). I found most devs considered these modules confusing, so I removed them. If you use newlib
and require dynamic memory allocation, you can implemente _sbrk()
syscall and use the standard malloc()
and free()
functions. You can also use SGDK that already includes a dynamic memory allocator. As for the loop
module, it has somehow been replaced by the tsk
module.
This module implements basic multitasking capabilities. It allows setting up a task that runs in user mode, in addition to the supervisor task. For the user task to run, the supervisor task must give it some CPU time. This can be done in two ways:
tsk_super_pend()
: this causes the user task to resume execution. User task will keep running until it calls tsk_super_post()
, or the timeout specified in the tsk_supper_pend()
occurs. Then supervisor task will resume execution.tsk_user_yield()
: this causes the user task to resume execution. User task will keep running until the next VBLANK interrupt, or until tsk_super_post()
is called with the force_ctx_sw
param set to true
.User task will not run unless the supervisor task calls either tsk_super_pend()
or tsk_user_yield()
. So you must make sure to call one of them in your main loop or the user task will starve. If you are using SGDK, the tsk_user_yield()
call is done transparently inside VDP_waitVSync()
, VDP_waitVActive()
and SYS_doVBlankProcess()
. So just make sure you are calling one of these in your loop and you should be ready to go!
A typical Megadrive game contains a main loop with a structure similar to this:
The game performs the initialization using init()
function, and then enters an infinite loop that:
tsk_user_yield()
must be called, either directly or indirectly (if you are using SGDK, as explained above).The order of these elements might be slightly different, but these are the usual suspects in loop game design.
And we finally arrive to the megawifi
module API. This API allows of course sending and receiving data to/from the Internet, along with some more functions such as:
You can use this API the hard way (directly sending commands defined in mw-msg
), or the easy way (through the API calls in megawifi
). Of course the later is recommended.
Most API functions require sending and receiving data to/from the WiFi module. But the data send/reception is decoupled from the command functions: the API functions prepare the module to send/receive data, but the data is not sent/received until the mw_process()
function is called. As the mw_process()
function polls the WiFi module for data, it is advisable to run it as frequently as possible. The easiest way to do this, is creating a user task that continuously runs mw_process()
:
Using a task like this makes sending and receiving data during game idle time way easier. Just make sure you have some spare CPU time for the user task to run and call mw_process()
. Otherwise this task will starve and no data will be sent/received!
About the API calls, basically all of them are synchronous or pseudo-synchronous, excepting the following ones, that are asynchronous and use callbacks to signal task completion:
mw_send()
: Send data to the other socket end.mw_recv()
: Receive data from the other socket end.mw_cmd_send()
: Send a command to the WiFi module.mw_cmd_recv()
: Receive a command reply from the WiFi module.Usually mw_cmd_send()
and mw_cmd_recv()
are not needed unless you decide to go down the hard path (using mw-msg
to build commands yourself). For sending/receiving data, it's up to you using the asynchronous mw_send()
and mw_recv()
or their pseudo-synchronous counterparts mw_send_sync()
and mw_recv_sync()
.
To save precious RAM, command functions reuse the same buffer. Thus when a command reply is obtained, you have to copy the needed data from the buffer before issuing another command. Otherwise the data in the previously received buffer will be lost.
In this section several examples explaining how to code typical tasks are presented.
MegaWiFi modules have 3 configuration slots, allowing to store 3 different network configurations. The configuration parameters are:
mw_ap_cfg_set()
. This usually requires a previous AP scan using mw_ap_scan()
and cycling through scan results using mw_ap_fill_next()
.mw_ip_cfg_set()
. Both automatic (DHCP) and manual configurations are supported.The good news is that you do not need to code the connection configuration, you can use the wflash bootloader to configure the network. As the configuration is stored inside the module, you can use it from your game, even if you delete the wflash bootloader ROM from the MegaWiFi cartridge.
Basically you have to initialize megawifi and the game loop as explained before. You also have to create a user task to run mw_process()
. The code below shows how to do this, and also how to detect if the WiFi module is installed, along with its firmware version.
Once configured, associating to an AP is easy. Just call mw_ap_assoc()
with the desired configuration slot, and the module will start the process. You can wait until the association is successful or fails (because of timeout) by calling mw_ap_assoc_wait()
. The following code tries to associate to an AP during 30 seconds (fps must be set previously to 60 on NTSC machines or 50 on PAL machines).
Once association has succeeded, you can try connecting to a server, or creating a server socket. DNS service will also start automatically after associating to the AP, but it takes a little bit more time.
Connecting to a server is straightforward: just call mw_tcp_connect()
with the channel to use, the destination address (both IPv4 addresses and domain names are supported), the destination port, and optionally the origin port (if NULL, it will be automatically set):
Once connected, you can start sending and receiving data. When no longer needed, remember to close the connection with mw_tcp_disconnect()
. The channel number must be from 1 to LSD_MAX_CH - 1
(usually 2). The used channel number will be passed to all the calls relative to the connected socket (think about it like a socket number).
Creating a TCP server socket requires binding it to a port, using mw_tcp_bind()
. After this, MegaWiFi will automatically accept any incoming connection on this port. You can check when the connection has been established by calling mw_sock_conn_wait()
:
You can send data once a connection has been established. The easiest way is using the synchronous variant, but as it suspends the execution of the calling function until data is sent, sometimes the asynchronous version is more convenient. The following code shows how to send data buffer of data_length length using channel 1, with a two second timeout:
The same data can be sent this way using the asynchronous API:
When using the asynchronous API, sometimes you do not need confirmation about when data has been sent. In that case, you do not need to use a completion callback, you can call mw_send()
with this parameter set to NULL.
You can receive data once a connection has been established. The easiest way is using the synchronous variant, but as it suspends the execution of the calling function until data is received, sometimes the asynchronous version is more convenient. The following code shows how to receive data buffer of buf_length maximum length using channel 1, with a 30 second timeout:
Note that when the function successfully returns, buf_length contains the number of bytes received.
The same data can be received this way using the asynchronous API:
megawifi
module allows performing HTTP and HTTPS requests in a simple way. HTTP and HTTPS use the same API, the only difference is that when using HTTPS, you can set an SSL certificate for the server identity to be verified. You can skip this step when using plain HTTP. Performing an HTTPS request requires the following steps. Some of them are optional and depend on the use case.
mw_http_cert_query()
. To set a different certificate, call mw_http_cert_set()
. Once set, the certificate is stored on the non volatile memory, and will remain until replaced with a new one. Only one certificate can be stored at a time. Note you should not use this function unless required, because as it writes to Flash memory, it can wear the storage if used too often.mw_http_url_set()
for this purpose.MW_HTTP_METHOD_GET
and MW_HTTP_METHOD_POST
. Use mw_http_method_set()
to set it.mw_http_open()
does this.mw_send()
or mw_send_sync()
functions, using the HTTP reserved channel (MW_CH_HTTP
).mw_http_finish()
for the HTTP client to obtain the response to the request, along with its associated headers. If a response includes a data payload, its length is obtained in this step.mw_recv()
or mw_recv_sync()
using the HTTP reserved channel (MW_CH_HTTP
).By looking to this list of steps, it might seem complicated to perform an HTTP request, but the steps are relatively simple, and can be easily added to some higher level functions. E.g., this code allows performing arbritrary GET
(without payload) and POST
(with JSON
payload) requests:
In case you want HTTPS, you can set a PEM formatted certificate as follows:
This function only sets the certificate if it has not been previously stored, avoiding to unnecessarily wear the Flash memory. To obtain a correct certificate hash, you can use openssl:
MegaWiFi allows to synchronize the date and time to NTP servers. It is important to note that on console power up, the module date and time will be incorrect and should not be used. For the date and time to be synchronized, the module must be associated to an AP with Internet connectivity. Once associated, the date and time is automatically synchronized. The synchronization procedure usually takes only a few seconds, and once completed, date/time should be usable until the console is powered off.
To guess if the date and time is in sync, you can check the dt_ok
field of mw_msg_sys_stat
union, by calling mw_sys_stat_get()
:
Once date and time is synchronized, you can get it, both in human readable format, and in the number of seconds elapsed since the epoch, with a single call to mw_date_time_get()
:
For the date/time to work properly, the timezone and NTP servers must be properly set. This is done calling mw_sntp_cfg_set()
, and configuration is stored in non volatile flash to survive power cycles, so you should do this only once. As an example, you can set the time configuration for the eastern Europe timezone (GMT-1 when not in dailight time) as follows:
MegaWiFi API allows to store and retrieve up to 3 gamertags. The gamertag information is contained in the mw_gamertag structure. This structure holds the gamertag unique identifier, nickname, security credentials (password) and a 32x48 avatar (tile information and palette). This example shows how to set a gamertag (excepting the graphics data):
To read the gamertag, just call mw_gamertag_get()
function, and the information corresponding to the requested slot will be returned:
The module has two network interfaces, each one with its unique BSSID (MAC address). One interface is used for the station mode, while the other is for the AP mode. Each BSSID is 6-byte long. Currently the API does not allow using the AP mode, so to get the station mode BSSID you can do as follows:
In addition to the standard 32 megabits of Flash ROM memory connected to the Megadrive 68k bus, MegaWiFi cartridges have 16 megabits of additional flash storage, directly usable by the game. This memory is organized in 4 KiB sectors, and supports the following operations:
mw_flash_id_get()
to obtain the flash memory identifiers. Usually not needed.mw_flash_sector_erase()
to erase an entire 4 KiB sector. Erased sectors will be read as 0xFF
.mw_flash_write()
to write the specified data buffer to the indicated address. Prior to programming, make sure the programmed address range is previously erased, otherwise operation will fail.mw_flash_read()
to read the specified amount of data from the indicated address.This functions can be used e.g. for high score keeping or DLCs. When using these functions, you have to keep in mind that flash can only be erased in a 1 sector (i.e. 4 KiB) granularity, and thus if e.g. you want to keep high scores, to update one of the high scores, you will have to erase the complete sector, and write it again in its entirety.
Also keep in mind that flash memory suffers from wearing, so do not perform more writes than necessary.
GameJolt API is implemented on top of the HTTP APIs, so the HTTP reserved channel is used to receive data when using the GameJolt API module. The current version 1.2 is fully supported, excepting the batch function, that maybe will not be very useful on the Megadrive, because of the tight memory constraints. It is recommended you complement this documentation with the official documentation of the API. You will find additional details there.
The first thing you need to know is that the GameJolt API implementation for MegaWiFi has the following restrictions:
gj_init()
), the receive buffer is configured. To avoid wasting RAM, usually you will want to use the same buffer used for all other communication routines (the one specified on mw_init()
invocation for example). When an API call returns pointers to data received from server, they point directly to different regions of this buffer. This means that before reusing the buffer again (i.e. before doing any other MegaWiFi call), you must copy all the data you want to preserve, or it will vanish before your eyes.gj_trophies_fetch()
and gj_users_fetch()
if you get data for several users in a row). It is the developer work to make sure the data will fit in the buffer (otherwise the API call will fail). For example if you create a lot of trophies for your game, you have to set the buffer length accordingly. Pay very careful attention to this, or your game online functions might fail unexpectedly.Batch
command is not supported.Other than these, the API is full featured, and I'm sure you will be able to do really cool things with it!
Here are some examples on using the API. Not all the supported features are shown, but for sure you will get an idea on how to use them. As mentioned earlier, do not forget to also check the official API documentation.
To initialize the API, you have to pass the endpoint, the game and user credentials, the buffer and the timeout to use for requests. It is recommended to let user configure the credentials in the gamertag slots, and get them from there using mw_gamertag_get()
before calling gj_init()
.
Make sure you store the credentials in a safe place, specially the game private key.
Trophies are added using the GameJolt Web UI. Once added, to make the player achieve a trophy, just call gj_trophy_add_achieved()
with the trophy id:
You can get all the available trophies, the ones achieved by the player, or a single one by id. Then you have to iterate on the returned data to get the trophy data one by one:
When posting scores, you can put them on the global game score table, or on any other additional table the developer has created for the game. You can also post them as the user, or as a guest (when allowed), and you must post both a textual representation of the score and a numeric sort value (but both can be the same). Also optionally, extra data can be added with the score:
You can retrieve scores from the main game table, or from any other additional tables the developer has created (using the GameJolt Web UI). First you get the raw table data, and then you iterate on scores one by one. When requesting the data, there are also some options to filter the values you get, read the documentation if you need to use them:
Sessions allow to track who is playing the game and how much time. Basically you open a session, and have to periodically ping it. If you spend 120 seconds without pinging the session, it will be automatically closed, so the recommendation is to ping each 30 seconds.
You can list friends, and get detailed data of each user. To list friends, request the raw data, and then iterate on the results, one by one:
And you can get user data from username or user_id as follows:
You can upload data to retrieve it later. And you can even make the cloud perform basic operations on the uploaded data. This can have many uses, such as saving detailed statistics, implement cloud save game functions, make turn based multiplayer games, etc. The data can be stored on the global game store, or on the user store. An example to store data is as follows:
To update data and perform operations on it, you can for example:
You can match keys to retrieve them using the wildcard character ('*'):
Most API functions return an error either via a bool value (error if true), or a data pointer (error if NULL). Internally the functions track the error with greater detail. If you want to know what caused the error, after a function fails, call gj_get_error()
. This will allow you to know if the error was caused because of a parameter error, a request error, a server error, etc. As the internally tracked error is updated after each GameJolt API call, you have to call this function just after the one that failed.
The main.c file contains a test program that detects the WiFi module, associates to the AP on slot 0, connects to https://www.example.com
using both a TCP socket and an HTTPS request, displays the synchronized date/time, sends and receives using a client UDP socket, and echoes UDP data on port 8007.
As previously discussed, most MegaWiFi API calls are synchronous: this means that when you call the function, the system task will be blocked until a response arrives (or a timeout occurs). This can be inconvenient, because typically you will still want to do things while waiting for the data (move backgrounds, update sprites, etc.). There are several ways to workaround this problem, some of them discussed below.
Usually you can separate the communications related work in two parts: initialization stage and game stage. During the initialization stage you will have to do tasks such as associating to the WiFi access point, connecting to the server, updating scoreboards, etc. During the game stage you will usually only need to send player actions to the server and get server status updates. Usually during the initialization stage it can be tolerable blocking and not updating the screen for short time-defined periods. But during game stage, usually yow want to continuously move things, so you cannot afford blocking the supervisor task. Taking this into account, a good compromise solution could be using all the MegaWiFi blocking functions during the initialization stage, and using the non-blocking asynchronous mw_send()
and mw_recv()
functions during the game stage.
You can use the VBLANK interrupt as an additional task to the supervisor and user task. The idea here is that the VBLANK interrupt performs all the game logic and commands the system task (via shared memory) when to send/receive data. When the data is sent/received, the code in the VBLANK interrupt will also get the notification/data via shared memory variables. The tricky part here is separating the game logic from the communications logic, and properly synchronizing the shared memory variables. Also you should make sure not touching the VDP outside the VBLANK interrupt, or you risk the code touching the VDP to be interrupted, leading to difficult to debug bugs.
Instead of using the "higher level" MegaWiFi API, that implements the locking mechanism and requires the user task to be properly configured, you can do your alternative implementation using only the asynchronous API calls:mw_send()
, mw_recv()
, mw_cmd_send()
and mw_cmd_recv()
. This requires manually building the command frames using the formatting defined in mw-msg.h
, and manually polling the mw_process()
function after any of the previous functions to get the data sent/received. Note this can be very time consuming if you are using many commands that you will have to implement, and especially for the more higher level ones, like the GameJolt Game API.
As I wrote above, previous versions of MegaWiFi came with a loop
module. This module is a bit more complex and requires a bit more work to set up than the tasking approach, but it can be a lot more flexible: it allows setting up as many "loop functions" and "loop timers" as you need. So you can have a loop timer blocked on sending/receiving data from the module, while other is running free and updating the game without a problem.
Although the loop module is not included in current MegaWiFi releases, you can grab it from older releases (latest one including it is version 1.4). If you do, make sure you also read the documentation from that release detailing how the loop module works.
As you have seen, you are in charge of setting up the user task to call mw_process()
. But in addition to call mw_process()
, you can do more work in the user task if you want. Your user task could be something like:
Inside update_game_logic()
you could monitor when the frame changes, and do updates to the game logic accordingly. Note this might look like an easy approach, but actually it might be the hardest one to do properly. The reason is that if you touch the VDP inside update_game_logic()
, if the function is interrupted in the middle of changing the VDP state, weird and hard to debug bugs will occur. I would advice against this approach, unless you do a thorough timing analysis to ensure this problem cannot occur. Also in case you were thinking on disabling interrupts at the CPU level while touching the VDP to avoid this problem, I'm afraid you can not. Changing interrupts is a privileged operation, and the user task just can't do it.
This API and documentation has been written by Jesús Alonso (doragasu).
Contributions are welcome. If you find a bug please open an issue, and if you have implemented a cool feature/improvement, please send a pull request.
The code and documentation in this repository is provided with NO WARRANTY, under the Mozilla Public License (MPL).