Add support for uploading animations to the panel, if it supports it.
This commit is contained in:
parent
0b3202ccff
commit
39cfc8cee0
@ -137,8 +137,9 @@ enum SMXUpdateCallbackReason {
|
||||
|
||||
// Bits for SMXConfig::flags.
|
||||
enum SMXConfigFlags {
|
||||
// This is used to store whether SMXConfig is in GIF animation mode or
|
||||
// not. If set, SMXConfig will use animations.
|
||||
// If set, panels will use the pressed animation when pressed, and stepColor
|
||||
// is ignored. If unset, panels will be lit solid using stepColor.
|
||||
// masterVersion >= 4. Previous versions always use stepColor.
|
||||
PlatformFlags_AutoLightingUsePressedAnimations = 1 << 0,
|
||||
};
|
||||
|
||||
@ -234,7 +235,7 @@ struct SMXConfig
|
||||
// This is disabled by default.
|
||||
uint16_t debounceDelayMs = 0;
|
||||
|
||||
// Packed flags (currently only used by SMXConfig).
|
||||
// Packed flags (masterVersion >= 4).
|
||||
uint8_t flags = 0;
|
||||
|
||||
// Pad the struct to 250 bytes. This keeps this struct size from changing
|
||||
|
@ -202,6 +202,14 @@ double SMX::GetMonotonicTime()
|
||||
return iTime / 10000000.0;
|
||||
}
|
||||
|
||||
const char *SMX::CreateError(string error)
|
||||
{
|
||||
// Store the string in a static so it doesn't get deallocated.
|
||||
static string buf;
|
||||
buf = error;
|
||||
return buf.c_str();
|
||||
}
|
||||
|
||||
SMX::AutoCloseHandle::AutoCloseHandle(HANDLE h)
|
||||
{
|
||||
handle = h;
|
||||
|
@ -27,6 +27,12 @@ string BinaryToHex(const string &sString);
|
||||
bool GetRandomBytes(void *pData, int iBytes);
|
||||
double GetMonotonicTime();
|
||||
|
||||
// Create a char* string that will be valid until the next call to CreateError.
|
||||
// This is used to return error messages to the caller.
|
||||
const char *CreateError(string error);
|
||||
|
||||
#define arraylen(a) (sizeof(a) / sizeof((a)[0]))
|
||||
|
||||
// In order to be able to use smart pointers to fully manage an object, we need to get
|
||||
// a shared_ptr to pass around, but also store a weak_ptr in the object itself. This
|
||||
// lets the object create shared_ptrs for itself as needed, without keeping itself from
|
||||
|
@ -23,6 +23,7 @@
|
||||
<ClInclude Include="SMXManager.h" />
|
||||
<ClInclude Include="SMXThread.h" />
|
||||
<ClInclude Include="SMXPanelAnimation.h" />
|
||||
<ClInclude Include="SMXPanelAnimationUpload.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="Helpers.cpp" />
|
||||
@ -36,6 +37,7 @@
|
||||
<ClCompile Include="SMXManager.cpp" />
|
||||
<ClCompile Include="SMXThread.cpp" />
|
||||
<ClCompile Include="SMXPanelAnimation.cpp" />
|
||||
<ClCompile Include="SMXPanelAnimationUpload.cpp" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{C5FC0823-9896-4B7C-BFE1-B60DB671A462}</ProjectGuid>
|
||||
|
@ -43,6 +43,9 @@
|
||||
<ClInclude Include="SMXPanelAnimation.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="SMXPanelAnimationUpload.h">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="SMX.cpp">
|
||||
@ -78,5 +81,8 @@
|
||||
<ClCompile Include="SMXPanelAnimation.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="SMXPanelAnimationUpload.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
@ -398,6 +398,11 @@ void SMX::SMXManager::SendLightUpdates()
|
||||
m_aPendingCommands.erase(m_aPendingCommands.begin(), m_aPendingCommands.begin()+1);
|
||||
}
|
||||
|
||||
void SMX::SMXManager::RunInHelperThread(function<void()> func)
|
||||
{
|
||||
m_UserCallbackThread.RunInThread(func);
|
||||
}
|
||||
|
||||
// See if there are any new devices to connect to.
|
||||
void SMX::SMXManager::AttemptConnections()
|
||||
{
|
||||
|
@ -46,6 +46,9 @@ public:
|
||||
void SetLights(const string sLights[2]);
|
||||
void ReenableAutoLights();
|
||||
|
||||
// Run a function in the user callback thread.
|
||||
void RunInHelperThread(function<void()> func);
|
||||
|
||||
private:
|
||||
static DWORD WINAPI ThreadMainStart(void *self_);
|
||||
void ThreadMain();
|
||||
|
428
sdk/Windows/SMXPanelAnimationUpload.cpp
Normal file
428
sdk/Windows/SMXPanelAnimationUpload.cpp
Normal file
@ -0,0 +1,428 @@
|
||||
#include "SMXPanelAnimationUpload.h"
|
||||
#include "SMXPanelAnimation.h"
|
||||
#include "SMXGif.h"
|
||||
#include "SMXManager.h"
|
||||
#include "SMXDevice.h"
|
||||
#include "Helpers.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
using namespace std;
|
||||
using namespace SMX;
|
||||
|
||||
// This handles setting up commands to upload panel animations to the
|
||||
// controller.
|
||||
//
|
||||
// This is only meant to be used by configuration tools to allow setting
|
||||
// up animations that work while the pad isn't being controlled by the
|
||||
// SDK. If you want to control lights for your game, this isn't what
|
||||
// you want. Use SMX_SetLights instead.
|
||||
//
|
||||
// Panel animations are sent to the master controller one panel at a time, and
|
||||
// each animation can take several commands to upload to fit in the protocol packet
|
||||
// size. These commands are stateful.
|
||||
|
||||
// XXX: should be able to upload both pads in parallel
|
||||
// XXX: we can only update all animations in one go, so save the last loaded animations
|
||||
// so the user doesn't have to manually load both to change one of them
|
||||
// do this in SMXConfig, not the SDK
|
||||
|
||||
namespace
|
||||
{
|
||||
// Panel names for error messages.
|
||||
static const char *panel_names[] = {
|
||||
"up-left", "up", "up-right",
|
||||
"left", "center", "right",
|
||||
"down-left", "down", "down-right",
|
||||
};
|
||||
}
|
||||
|
||||
// These structs are the protocol we use to send offline graphics to the pad.
|
||||
// This isn't related to realtime lighting.
|
||||
namespace PanelLightGraphic
|
||||
{
|
||||
// One 24-bit RGB color:
|
||||
struct color_t {
|
||||
uint8_t rgb[3];
|
||||
};
|
||||
|
||||
// 4-bit palette, 15 colors. Our graphics are 4-bit. Color 0xF is transparent,
|
||||
// so we don't have a palette entry for it.
|
||||
struct palette_t {
|
||||
color_t colors[15];
|
||||
};
|
||||
|
||||
// A single 4-bit paletted graphic.
|
||||
struct graphic_t {
|
||||
uint8_t data[13];
|
||||
};
|
||||
|
||||
struct panel_animation_data_t
|
||||
{
|
||||
// Our graphics and palettes. We can apply either palette to any graphic. Note that
|
||||
// each graphic is 13 bytes and each palette is 45 bytes.
|
||||
graphic_t graphics[64];
|
||||
palette_t palettes[2];
|
||||
};
|
||||
|
||||
struct animation_timing_t
|
||||
{
|
||||
// An index into frames[]:
|
||||
uint8_t loop_animation_frame;
|
||||
|
||||
// A list of graphic frames to display, and how long to display them in
|
||||
// 30 FPS frames. A frame index of 0xFF (or reaching the end) loops.
|
||||
uint8_t frames[64];
|
||||
uint8_t delay[64];
|
||||
};
|
||||
|
||||
struct master_animation_data_t
|
||||
{
|
||||
animation_timing_t animation_timings[2];
|
||||
};
|
||||
|
||||
// Commands to upload data:
|
||||
struct upload_packet
|
||||
{
|
||||
// 'm' to upload master animation data.
|
||||
uint8_t cmd = 'm';
|
||||
|
||||
// The panel this data is for. If this is 0xFF, it's for the master.
|
||||
uint8_t panel = 0;
|
||||
|
||||
// For master uploads, the animation number to modify. Panels ignore this field.
|
||||
uint8_t animation_idx = 0;
|
||||
|
||||
// True if this is the last upload packet. This lets the firmware know that
|
||||
// this part of the upload is finished and it can update anything that might
|
||||
// be affected by it, like resetting lights animations.
|
||||
bool final_packet = false;
|
||||
|
||||
uint8_t offset = 0, size = 0;
|
||||
uint8_t data[240];
|
||||
};
|
||||
|
||||
// Make sure the packet fits in a command packet.
|
||||
static_assert(sizeof(upload_packet) <= 0xFF, "");
|
||||
}
|
||||
|
||||
// The GIFs can use variable framerates. The panels update at 30 FPS.
|
||||
#define FPS 30
|
||||
|
||||
// Helpers for converting PanelGraphics to the packed sprite representation
|
||||
// we give to the pad.
|
||||
namespace ProtocolHelpers
|
||||
{
|
||||
// Return a color's index in palette. If the color isn't found, return 0.
|
||||
// We can use a dumb linear search here since the graphics are so small.
|
||||
uint8_t GetColorIndex(const PanelLightGraphic::palette_t &palette, const SMXGif::Color &color)
|
||||
{
|
||||
// Transparency is always palette index 15.
|
||||
if(color.color[3] == 0)
|
||||
return 15;
|
||||
|
||||
for(int idx = 0; idx < 15; ++idx)
|
||||
{
|
||||
PanelLightGraphic::color_t pad_color = palette.colors[idx];
|
||||
if(pad_color.rgb[0] == color.color[0] &&
|
||||
pad_color.rgb[1] == color.color[1] &&
|
||||
pad_color.rgb[2] == color.color[2])
|
||||
return idx;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Create a palette for an animation.
|
||||
//
|
||||
// We're loading from paletted GIFs, but we create a separate small palette
|
||||
// for each panel's animation, so we don't use the GIF's palette.
|
||||
bool CreatePalette(const SMXPanelAnimation &animation, PanelLightGraphic::palette_t &palette)
|
||||
{
|
||||
int next_color = 0;
|
||||
for(const auto &panel_graphic: animation.m_aPanelGraphics)
|
||||
{
|
||||
for(const SMXGif::Color &color: panel_graphic)
|
||||
{
|
||||
// If this color is transparent, leave it out of the palette.
|
||||
if(color.color[3] == 0)
|
||||
continue;
|
||||
|
||||
// Check if this color is already in the palette.
|
||||
uint8_t existing_idx = GetColorIndex(palette, color);
|
||||
if(existing_idx < next_color)
|
||||
continue;
|
||||
|
||||
// Return false if we're using too many colors.
|
||||
if(next_color == 15)
|
||||
return false;
|
||||
|
||||
// Add this color.
|
||||
PanelLightGraphic::color_t pad_color;
|
||||
pad_color.rgb[0] = color.color[0];
|
||||
pad_color.rgb[1] = color.color[1];
|
||||
pad_color.rgb[2] = color.color[2];
|
||||
palette.colors[next_color] = pad_color;
|
||||
next_color++;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Return packed paletted graphics for each frame, using a palette created
|
||||
// with CreatePalette. The palette must have fewer than 16 colors.
|
||||
void CreatePackedGraphic(const vector<SMXGif::Color> &image, const PanelLightGraphic::palette_t &palette,
|
||||
PanelLightGraphic::graphic_t &out)
|
||||
{
|
||||
int position = 0;
|
||||
for(auto color: image)
|
||||
{
|
||||
// Transparency is always palette index 15.
|
||||
uint8_t palette_idx = GetColorIndex(palette, color);
|
||||
|
||||
// Apply color scaling, in the same way SMXManager::SetLights does.
|
||||
for(int i = 0; i < 3; ++i)
|
||||
color.color[i] = uint8_t(color.color[i] * 0.6666f);
|
||||
|
||||
// If this is an odd index, put the palette index in the high 4
|
||||
// bits. Otherwise, put it in the low 4 bits.
|
||||
if(position & 1)
|
||||
out.data[position/2] |= (palette_idx & 0xF0) << 4;
|
||||
else
|
||||
out.data[position/2] |= (palette_idx & 0xF0) << 0;
|
||||
}
|
||||
}
|
||||
|
||||
vector<uint8_t> get_frame_delays(const SMXPanelAnimation &animation)
|
||||
{
|
||||
vector<uint8_t> result;
|
||||
int current_frame = 0;
|
||||
|
||||
int time_left_in_frame = animation.m_iFrameDurations[0];
|
||||
result.push_back(0);
|
||||
while(1)
|
||||
{
|
||||
// Advance time by 1/FPS seconds.
|
||||
time_left_in_frame -= 1 / FPS;
|
||||
result.back()++;
|
||||
|
||||
if(time_left_in_frame <= 0.00001)
|
||||
{
|
||||
// We've displayed this frame long enough, so advance to the next frame.
|
||||
if(current_frame + 1 == animation.m_iFrameDurations.size())
|
||||
break;
|
||||
|
||||
current_frame += 1;
|
||||
result.push_back(0);
|
||||
time_left_in_frame += animation.m_iFrameDurations[current_frame];
|
||||
|
||||
// If time_left_in_frame is still negative, the animation is too fast.
|
||||
if(time_left_in_frame < 0.00001)
|
||||
time_left_in_frame = 0;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Create the master data. This just has timing information.
|
||||
bool CreateMasterAnimationData(int pad, PanelLightGraphic::master_animation_data_t &master_data, const char **error)
|
||||
{
|
||||
// The second animation's graphic indices start where the first one's end.
|
||||
int first_graphic = 0;
|
||||
|
||||
// XXX: It's possible to reuse graphics, which allows us to dedupe animation frames.
|
||||
// We can store 64 total animation graphics shared across the pressed and released
|
||||
// animation, and each animation can have up to 64 timed frames which point into
|
||||
// the graphic list.
|
||||
for(int type = 0; type < NUM_SMX_LightsType; ++type)
|
||||
{
|
||||
// All animations of each type have the same timing for all panels, since
|
||||
// they come from the same GIF, so just look at the first frame.
|
||||
const SMXPanelAnimation &animation = SMXPanelAnimation::GetLoadedAnimation(pad, 0, SMX_LightsType(type));
|
||||
|
||||
PanelLightGraphic::animation_timing_t &animation_timing = master_data.animation_timings[type];
|
||||
|
||||
// Check that we don't have more frames than we can fit in animation_timing.
|
||||
// This is currently the same as the "too many frames" error below, but if
|
||||
// we support longer delays (staying on the same graphic for multiple animation_timings)
|
||||
// or deduping they'd be different.
|
||||
if(animation.m_aPanelGraphics.size() > arraylen(animation_timing.frames))
|
||||
{
|
||||
*error = "The animation is too long.";
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(&animation_timing.frames[0], 0xFF, sizeof(animation_timing.frames));
|
||||
for(int i = 0; i < animation.m_aPanelGraphics.size(); ++i)
|
||||
{
|
||||
animation_timing.frames[i] = first_graphic;
|
||||
first_graphic++;
|
||||
}
|
||||
|
||||
// Set frame delays.
|
||||
memset(&animation_timing.delay[0], 0, sizeof(animation_timing.delay));
|
||||
vector<uint8_t> delays = get_frame_delays(animation);
|
||||
for(int i = 0; i < delays.size() && i < 64; ++i)
|
||||
animation_timing.delay[i] = delays[i];
|
||||
|
||||
// These frame numbers are relative to the animation, so don't add first_graphic.
|
||||
// XXX: frame index, not source frame
|
||||
animation_timing.loop_animation_frame = animation.m_iLoopFrame;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pack panel graphics.
|
||||
bool CreatePanelAnimationData(PanelLightGraphic::panel_animation_data_t &panel_data,
|
||||
int pad, int panel, const char **error)
|
||||
{
|
||||
// We have a single buffer of animation frames for each panel, which we pack
|
||||
// both the pressed and released frames into. This is the index of the next
|
||||
// frame.
|
||||
int next_graphic_idx = 0;
|
||||
|
||||
for(int type = 0; type < NUM_SMX_LightsType; ++type)
|
||||
{
|
||||
const SMXPanelAnimation &animation = SMXPanelAnimation::GetLoadedAnimation(pad, panel, SMX_LightsType(type));
|
||||
|
||||
// Create this animation's 4-bit palette.
|
||||
if(!ProtocolHelpers::CreatePalette(animation, panel_data.palettes[type]))
|
||||
{
|
||||
*error = SMX::CreateError(SMX::ssprintf("The %s panel uses too many colors.", panel_names[panel]));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a small 4-bit paletted graphic with the 4-bit palette we created.
|
||||
// These are the graphics we'll send to the controller.
|
||||
for(const auto &panel_graphic: animation.m_aPanelGraphics)
|
||||
{
|
||||
if(next_graphic_idx > arraylen(panel_data.graphics))
|
||||
{
|
||||
*error = "The animation has too many frames.";
|
||||
return false;
|
||||
}
|
||||
|
||||
ProtocolHelpers::CreatePackedGraphic(panel_graphic, panel_data.palettes[type], panel_data.graphics[next_graphic_idx]);
|
||||
next_graphic_idx++;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create upload packets to upload a block of data.
|
||||
void CreateUploadPackets(vector<PanelLightGraphic::upload_packet> &packets,
|
||||
const void *data_block, int size,
|
||||
uint8_t panel, int animation_idx)
|
||||
{
|
||||
const uint8_t *buf = (const uint8_t *) &data_block;
|
||||
for(int offset = 0; offset < size; )
|
||||
{
|
||||
PanelLightGraphic::upload_packet packet;
|
||||
packet.panel = panel;
|
||||
packet.animation_idx = animation_idx;
|
||||
packet.offset = offset;
|
||||
|
||||
int bytes_left = size - offset;
|
||||
packet.size = min(sizeof(PanelLightGraphic::upload_packet::data), bytes_left);
|
||||
memcpy(packet.data, buf + offset, packet.size);
|
||||
packets.push_back(packet);
|
||||
|
||||
offset += packet.size;
|
||||
}
|
||||
|
||||
packets.back().final_packet = true;
|
||||
}
|
||||
}
|
||||
|
||||
namespace LightsUploadData
|
||||
{
|
||||
vector<string> commands[2];
|
||||
}
|
||||
|
||||
// Prepare the loaded graphics for upload.
|
||||
bool SMX_LightsUpload_PrepareUpload(int pad, const char **error)
|
||||
{
|
||||
// Check that all panel animations are loaded.
|
||||
for(int type = 0; type < NUM_SMX_LightsType; ++type)
|
||||
{
|
||||
const SMXPanelAnimation &animation = SMXPanelAnimation::GetLoadedAnimation(pad, 0, SMX_LightsType(type));
|
||||
if(animation.m_aPanelGraphics.empty())
|
||||
{
|
||||
*error = "Load all panel animations before preparing the upload.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Create master animation data.
|
||||
PanelLightGraphic::master_animation_data_t master_data;
|
||||
if(!ProtocolHelpers::CreateMasterAnimationData(pad, master_data, error))
|
||||
return false;
|
||||
|
||||
// Create panel animation data.
|
||||
PanelLightGraphic::panel_animation_data_t all_panel_data[9];
|
||||
for(int panel = 0; panel < 9; ++panel)
|
||||
{
|
||||
if(!ProtocolHelpers::CreatePanelAnimationData(all_panel_data[panel], pad, panel, error))
|
||||
return false;
|
||||
}
|
||||
|
||||
// We successfully created the data, so there's nothing else that can fail from
|
||||
// here on.
|
||||
|
||||
// Create upload packets.
|
||||
vector<PanelLightGraphic::upload_packet> packets;
|
||||
for(int type = 0; type < NUM_SMX_LightsType; ++type)
|
||||
{
|
||||
const auto &master_data_block = master_data.animation_timings[type];
|
||||
ProtocolHelpers::CreateUploadPackets(packets, &master_data_block, sizeof(master_data_block), 0xFF, type);
|
||||
|
||||
for(int panel = 0; panel < 9; ++panel)
|
||||
{
|
||||
const auto &panel_data_block = all_panel_data[panel];
|
||||
ProtocolHelpers::CreateUploadPackets(packets, &panel_data_block, sizeof(panel_data_block), panel, type);
|
||||
}
|
||||
}
|
||||
|
||||
// Make a list of strings containing the packets. We don't need the
|
||||
// structs anymore, so this is all we need to keep around.
|
||||
vector<string> &pad_commands = LightsUploadData::commands[pad];
|
||||
pad_commands.clear();
|
||||
for(const auto &packet: packets)
|
||||
{
|
||||
string command((char *) &packet, sizeof(packet));
|
||||
pad_commands.push_back(command);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Start sending a prepared upload.
|
||||
//
|
||||
// The commands to send to upload the data are in pad_commands[pad].
|
||||
void SMX_LightsUpload_BeginUpload(int pad, SMX_LightsUploadCallback pCallback, void *pUser)
|
||||
{
|
||||
// XXX: should we disable panel lights while doing this?
|
||||
shared_ptr<SMXDevice> pDevice = SMXManager::g_pSMX->GetDevice(pad);
|
||||
vector<string> asCommands = LightsUploadData::commands[pad];
|
||||
int iTotalCommands = asCommands.size();
|
||||
|
||||
// Queue all commands at once. As each command finishes, our callback
|
||||
// will be called.
|
||||
for(int i = 0; i < asCommands.size(); ++i)
|
||||
{
|
||||
const string &sCommand = asCommands[i];
|
||||
pDevice->SendCommand(sCommand, [i, iTotalCommands, pCallback, pUser]() {
|
||||
// Command #i has finished being sent.
|
||||
//
|
||||
// If this isn't the last command, make sure progress isn't 100.
|
||||
// Once we send 100%, the callback is no longer valid.
|
||||
int progress = (i*100) / (iTotalCommands-1);
|
||||
if(i != iTotalCommands-1)
|
||||
progress = min(progress, 99);
|
||||
|
||||
// We're currently in the SMXManager thread. Call the user thread from
|
||||
// the user callback thread.
|
||||
SMXManager::g_pSMX->RunInHelperThread([pCallback, pUser, progress]() {
|
||||
pCallback(progress, pUser);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
40
sdk/Windows/SMXPanelAnimationUpload.h
Normal file
40
sdk/Windows/SMXPanelAnimationUpload.h
Normal file
@ -0,0 +1,40 @@
|
||||
#ifndef SMXPanelAnimationUpload_h
|
||||
#define SMXPanelAnimationUpload_h
|
||||
|
||||
#include "SMXPanelAnimation.h"
|
||||
|
||||
// For SMX_API:
|
||||
#include "../SMX.h"
|
||||
|
||||
// This is used to upload panel animations to the firmware. This is
|
||||
// only needed for offline animations. For live animations, either
|
||||
// use SMX_LightsAnimation_SetAuto, or to control lights directly
|
||||
// (recommended), use SMX_SetLights.
|
||||
//
|
||||
// Before starting, load animations into SMXPanelAnimation.
|
||||
//
|
||||
// Prepare the currently loaded animations to be stored on the pad.
|
||||
// Return false with an error message on error.
|
||||
//
|
||||
// All LightTypes must be loaded before beginning the upload.
|
||||
//
|
||||
// If a lights upload is already in progress, returns an error.
|
||||
SMX_API bool SMX_LightsUpload_PrepareUpload(int pad, const char **error);
|
||||
|
||||
typedef void SMX_LightsUploadCallback(int progress, void *pUser);
|
||||
|
||||
// After a successful call to SMX_LightsUpload_Init, begin uploading data
|
||||
// to the master controller for the given pad and animation type.
|
||||
//
|
||||
// The callback will be called as the upload progresses, with progress values
|
||||
// from 0-100.
|
||||
//
|
||||
// callback will always be called exactly once with a progress value of 100.
|
||||
// Once the 100% progress is called, the callback won't be accessed, so the
|
||||
// caller can safely clean up. This will happen even if the pad disconnects
|
||||
// partway through the upload.
|
||||
//
|
||||
// The callback will be called from the user callback helper thread.
|
||||
SMX_API void SMX_LightsUpload_BeginUpload(int pad, SMX_LightsUploadCallback callback, void *pUser);
|
||||
|
||||
#endif
|
@ -58,6 +58,7 @@ namespace smx_config
|
||||
// Load animations, and tell the SDK to handle auto-lighting as long as
|
||||
// we're running.
|
||||
Helpers.LoadSavedPanelAnimations();
|
||||
Helpers.PrepareLoadedAnimations();
|
||||
SMX.SMX.LightsAnimation_SetAuto(true);
|
||||
|
||||
CreateTrayIcon();
|
||||
|
@ -345,6 +345,28 @@ namespace smx_config
|
||||
SMX.SMX.LightsAnimation_Load(gif, pad, type, out error);
|
||||
}
|
||||
|
||||
// Prepare all loaded animations for upload.
|
||||
//
|
||||
// We do this early, as soon as animations are loaded, so we know whether there are
|
||||
// any errors preventing upload to display in the UI.
|
||||
public static void PrepareLoadedAnimations()
|
||||
{
|
||||
PanelLoadErrors = null;
|
||||
// Prepare animations for both pads.
|
||||
for(int pad = 0; pad < 2; ++pad)
|
||||
{
|
||||
// Store the first error we get. Keep loading pad 1 even if pad 0 has
|
||||
// an error.
|
||||
string error;
|
||||
if(!SMX.SMX.LightsUpload_PrepareUpload(pad, out error) && PanelLoadErrors == null)
|
||||
PanelLoadErrors = error;
|
||||
}
|
||||
}
|
||||
|
||||
// If there was an error preparing animations, we store it here for display. Otherwise,
|
||||
// this is null.
|
||||
public static string PanelLoadErrors = null;
|
||||
|
||||
// Create a .lnk.
|
||||
public static void CreateShortcut(string outputFile, string targetPath, string arguments)
|
||||
{
|
||||
|
@ -7,7 +7,7 @@
|
||||
xmlns:controls="clr-namespace:smx_config"
|
||||
mc:Ignorable="d"
|
||||
x:Name="root"
|
||||
Title="StepManiaX Platform Settings"
|
||||
Title="StepManiaX Platform"
|
||||
Icon="Resources/window icon.png"
|
||||
Height="700" Width="525" ResizeMode="CanMinimize">
|
||||
<Window.Resources>
|
||||
@ -696,9 +696,27 @@ Use if the platform is too sensitive.</clr:String>
|
||||
<Button Content="Load pressed GIF" Padding="5,2" Margin="5,10" Name="LoadPressed" Click="LoadGIF" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- We'll only show one of these. -->
|
||||
<TextBlock Name="LeaveRunning" TextAlignment="Center">
|
||||
Leave this application running to play animations.
|
||||
</TextBlock>
|
||||
<StackPanel Name="LeaveRunningOrUpload"
|
||||
HorizontalAlignment="Center" Orientation="Vertical">
|
||||
<TextBlock TextAlignment="Center">
|
||||
Leave this application running to play animations, or upload
|
||||
it to the pad.
|
||||
</TextBlock>
|
||||
|
||||
<Button Content="Upload animation to pad" Padding="5,2" Margin="5,10"
|
||||
Click="UploadGIFs" HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<TextBlock Name="LeaveRunningCantUpload" TextAlignment="Center" Width="400" TextWrapping="Wrap">
|
||||
<TextBlock>This animation is too big to upload it to the pad:</TextBlock>
|
||||
<LineBreak/>
|
||||
<TextBlock Name="UploadErrorReason"></TextBlock>.
|
||||
<LineBreak/>
|
||||
<TextBlock Margin="0,5,0,0">However, you can leaving this application running to use it.</TextBlock>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
|
@ -19,9 +19,11 @@ namespace smx_config
|
||||
LoadUIFromConfig(args);
|
||||
});
|
||||
|
||||
// If we're controlling GIF animations, confirm exiting, since you can minimize
|
||||
// If we're controlling GIF animations and the firmware doesn't support
|
||||
// doing animations internally, confirm exiting, since you can minimize
|
||||
// to tray to keep playing animations. If we're not controlling animations,
|
||||
// don't bug the user with a prompt.
|
||||
// or the firmware supports doing them automatically, don't bug the user
|
||||
// with a prompt.
|
||||
Closing += delegate(object sender, System.ComponentModel.CancelEventArgs e)
|
||||
{
|
||||
LoadFromConfigDelegateArgs args = CurrentSMXDevice.singleton.GetState();
|
||||
@ -36,6 +38,13 @@ namespace smx_config
|
||||
if(!SMX.SMX.GetConfig(pad, out config))
|
||||
continue;
|
||||
|
||||
// If the firmware is version 4 or higher, it supports animations directly.
|
||||
// The user can upload GIF animations and doesn't need to leave us running
|
||||
// for them to work. You can still use this tool to drive animations, but
|
||||
// don't confirm exiting.
|
||||
if(config.masterVersion >= 4)
|
||||
continue;
|
||||
|
||||
// If AutoLightingUsePressedAnimations isn't set, the panel is using step
|
||||
// coloring instead of pressed animations. All firmwares support this.
|
||||
// Don't confirm exiting for this mode.
|
||||
@ -167,6 +176,7 @@ namespace smx_config
|
||||
}
|
||||
|
||||
RefreshConnectedPadList(args);
|
||||
RefreshUploadPadText(args);
|
||||
|
||||
// If a second controller has connected and we're on Both, see if we need to prompt
|
||||
// to sync configs. We only actually need to do this if a controller just connected.
|
||||
@ -174,6 +184,28 @@ namespace smx_config
|
||||
CheckConfiguringBothPads(args);
|
||||
}
|
||||
|
||||
// Update which of the "Leave this application running", etc. blocks to display.
|
||||
private void RefreshUploadPadText(LoadFromConfigDelegateArgs args)
|
||||
{
|
||||
foreach(Tuple<int, SMX.SMXConfig> activePad in ActivePad.ActivePads())
|
||||
{
|
||||
SMX.SMXConfig config = activePad.Item2;
|
||||
|
||||
bool uploadsSupported = config.masterVersion >= 4;
|
||||
bool uploadPossible = Helpers.PanelLoadErrors == null;
|
||||
|
||||
LeaveRunning.Visibility = uploadsSupported? Visibility.Collapsed:Visibility.Visible;
|
||||
LeaveRunningOrUpload.Visibility = uploadsSupported && uploadPossible? Visibility.Visible:Visibility.Collapsed;
|
||||
LeaveRunningCantUpload.Visibility = uploadsSupported && !uploadPossible? Visibility.Visible:Visibility.Collapsed;
|
||||
|
||||
// If we have an error reason, set it. This is only visible when
|
||||
// we're showing LeaveRunningCantUpload.
|
||||
if(Helpers.PanelLoadErrors != null)
|
||||
UploadErrorReason.Text = Helpers.PanelLoadErrors;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ConnectedPadList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
ComboBoxItem selection = ConnectedPadList.SelectedItem as ComboBoxItem;
|
||||
@ -384,9 +416,52 @@ namespace smx_config
|
||||
// Save the GIF to disk so we can load it quickly later.
|
||||
Helpers.SaveAnimationToDisk(pad, type, buf);
|
||||
|
||||
// Try to prepare animations for upload. This updates Helpers.PanelLoadErrors.
|
||||
Helpers.PrepareLoadedAnimations();
|
||||
|
||||
// Refresh after loading a GIF to update the "Leave this application running" text.
|
||||
CurrentSMXDevice.singleton.FireConfigurationChanged(null);
|
||||
}
|
||||
}
|
||||
|
||||
// The "Upload animation to pad" button was clicked.
|
||||
private void UploadGIFs(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Create a progress window. Center it on top of the main window.
|
||||
ProgressWindow dialog = new ProgressWindow();
|
||||
dialog.Left = (Left + Width/2) - (dialog.Width/2);
|
||||
dialog.Top = (Top + Height/2) - (dialog.Height/2);
|
||||
dialog.Title = "Storing animations on pad...";
|
||||
|
||||
int[] CurrentProgress = new int[] { 0, 0 };
|
||||
|
||||
// Upload graphics for all connected pads. If two pads are connected
|
||||
// we can start both of these simultaneously, and they'll be sent in
|
||||
// parallel.
|
||||
int total = 0;
|
||||
foreach(Tuple<int,SMX.SMXConfig> activePad in ActivePad.ActivePads())
|
||||
{
|
||||
int pad = activePad.Item1;
|
||||
SMX.SMX.LightsUpload_BeginUpload(pad, delegate(int progress) {
|
||||
// This is called from a thread, so dispatch back to the main thread.
|
||||
Dispatcher.Invoke(delegate() {
|
||||
// Store progress, so we can sum both pads.
|
||||
CurrentProgress[pad] = progress;
|
||||
|
||||
dialog.SetProgress(CurrentProgress[0] + CurrentProgress[1]);
|
||||
if(progress == 100)
|
||||
dialog.Close();
|
||||
});
|
||||
});
|
||||
|
||||
// Each pad that we start uploading to is 100 units of progress.
|
||||
total += 100;
|
||||
dialog.SetTotal(total);
|
||||
}
|
||||
|
||||
// Show the progress window as a modal dialog. This function won't return
|
||||
// until we call dialog.Close above.
|
||||
dialog.ShowDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -483,5 +483,63 @@ namespace SMX
|
||||
if(!DLLAvailable()) return;
|
||||
SMX_LightsAnimation_SetAuto(enable);
|
||||
}
|
||||
|
||||
// SMXPanelAnimationUpload
|
||||
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
[return:MarshalAs(UnmanagedType.I1)]
|
||||
private static extern bool SMX_LightsUpload_PrepareUpload(int pad, out IntPtr error);
|
||||
[DllImport("SMX.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
|
||||
private static extern void SMX_LightsUpload_BeginUpload(int pad,
|
||||
[MarshalAs(UnmanagedType.FunctionPtr)] InternalLightsUploadCallback callback,
|
||||
IntPtr user);
|
||||
|
||||
public static bool LightsUpload_PrepareUpload(int pad, out string error)
|
||||
{
|
||||
if(!DLLAvailable())
|
||||
{
|
||||
error = "SMX.DLL not available";
|
||||
return false;
|
||||
}
|
||||
|
||||
error = "";
|
||||
IntPtr error_pointer;
|
||||
bool result = SMX_LightsUpload_PrepareUpload(pad, out error_pointer);
|
||||
if(!result)
|
||||
{
|
||||
// SMX_LightsAnimation_Load takes a char **error, which is set to the error
|
||||
// string.
|
||||
error = Marshal.PtrToStringAnsi(error_pointer);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public delegate void LightsUploadCallback(int progress);
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void InternalLightsUploadCallback(int reason, IntPtr user);
|
||||
public static void LightsUpload_BeginUpload(int pad, LightsUploadCallback callback)
|
||||
{
|
||||
if(!DLLAvailable())
|
||||
return;
|
||||
|
||||
GCHandle handle = new GCHandle();
|
||||
InternalLightsUploadCallback wrapper = delegate(int progress, IntPtr user)
|
||||
{
|
||||
try {
|
||||
callback(progress);
|
||||
} finally {
|
||||
// When progress = 100, this is the final call and we can release this
|
||||
// object to GC.
|
||||
if(progress == 100)
|
||||
handle.Free();
|
||||
}
|
||||
};
|
||||
|
||||
// Pin the callback until we get the last call.
|
||||
handle = GCHandle.Alloc(wrapper);
|
||||
|
||||
SMX_LightsUpload_BeginUpload(pad, wrapper, IntPtr.Zero);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user