diff --git a/sdk/SMX.h b/sdk/SMX.h
index 574d2ec..ce8d9fb 100644
--- a/sdk/SMX.h
+++ b/sdk/SMX.h
@@ -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
diff --git a/sdk/Windows/Helpers.cpp b/sdk/Windows/Helpers.cpp
index a886e88..1fb8021 100644
--- a/sdk/Windows/Helpers.cpp
+++ b/sdk/Windows/Helpers.cpp
@@ -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;
diff --git a/sdk/Windows/Helpers.h b/sdk/Windows/Helpers.h
index 5b3d6f7..b9b90f8 100644
--- a/sdk/Windows/Helpers.h
+++ b/sdk/Windows/Helpers.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
diff --git a/sdk/Windows/SMX.vcxproj b/sdk/Windows/SMX.vcxproj
index d238052..2052e98 100644
--- a/sdk/Windows/SMX.vcxproj
+++ b/sdk/Windows/SMX.vcxproj
@@ -23,6 +23,7 @@
+
@@ -36,6 +37,7 @@
+ {C5FC0823-9896-4B7C-BFE1-B60DB671A462}
@@ -144,4 +146,4 @@
-
\ No newline at end of file
+
diff --git a/sdk/Windows/SMX.vcxproj.filters b/sdk/Windows/SMX.vcxproj.filters
index dca4a22..4ca4d76 100644
--- a/sdk/Windows/SMX.vcxproj.filters
+++ b/sdk/Windows/SMX.vcxproj.filters
@@ -43,6 +43,9 @@
Source Files
+
+ Source Files
+
@@ -78,5 +81,8 @@
Source Files
+
+ Source Files
+
diff --git a/sdk/Windows/SMXManager.cpp b/sdk/Windows/SMXManager.cpp
index 3473474..e483064 100644
--- a/sdk/Windows/SMXManager.cpp
+++ b/sdk/Windows/SMXManager.cpp
@@ -398,6 +398,11 @@ void SMX::SMXManager::SendLightUpdates()
m_aPendingCommands.erase(m_aPendingCommands.begin(), m_aPendingCommands.begin()+1);
}
+void SMX::SMXManager::RunInHelperThread(function func)
+{
+ m_UserCallbackThread.RunInThread(func);
+}
+
// See if there are any new devices to connect to.
void SMX::SMXManager::AttemptConnections()
{
diff --git a/sdk/Windows/SMXManager.h b/sdk/Windows/SMXManager.h
index 1c061f3..9f6c1e7 100644
--- a/sdk/Windows/SMXManager.h
+++ b/sdk/Windows/SMXManager.h
@@ -46,6 +46,9 @@ public:
void SetLights(const string sLights[2]);
void ReenableAutoLights();
+ // Run a function in the user callback thread.
+ void RunInHelperThread(function func);
+
private:
static DWORD WINAPI ThreadMainStart(void *self_);
void ThreadMain();
diff --git a/sdk/Windows/SMXPanelAnimationUpload.cpp b/sdk/Windows/SMXPanelAnimationUpload.cpp
new file mode 100644
index 0000000..8178e52
--- /dev/null
+++ b/sdk/Windows/SMXPanelAnimationUpload.cpp
@@ -0,0 +1,428 @@
+#include "SMXPanelAnimationUpload.h"
+#include "SMXPanelAnimation.h"
+#include "SMXGif.h"
+#include "SMXManager.h"
+#include "SMXDevice.h"
+#include "Helpers.h"
+#include
+#include
+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 &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 get_frame_delays(const SMXPanelAnimation &animation)
+ {
+ vector 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 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 &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 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 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 &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 pDevice = SMXManager::g_pSMX->GetDevice(pad);
+ vector 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);
+ });
+ });
+ }
+}
diff --git a/sdk/Windows/SMXPanelAnimationUpload.h b/sdk/Windows/SMXPanelAnimationUpload.h
new file mode 100644
index 0000000..94fd3f4
--- /dev/null
+++ b/sdk/Windows/SMXPanelAnimationUpload.h
@@ -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
diff --git a/smx-config/App.xaml.cs b/smx-config/App.xaml.cs
index fdf1bd2..4d13f2f 100644
--- a/smx-config/App.xaml.cs
+++ b/smx-config/App.xaml.cs
@@ -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();
diff --git a/smx-config/Helpers.cs b/smx-config/Helpers.cs
index 1a54393..aab0853 100644
--- a/smx-config/Helpers.cs
+++ b/smx-config/Helpers.cs
@@ -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)
{
diff --git a/smx-config/MainWindow.xaml b/smx-config/MainWindow.xaml
index c57970f..bda84d3 100644
--- a/smx-config/MainWindow.xaml
+++ b/smx-config/MainWindow.xaml
@@ -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">
@@ -696,9 +696,27 @@ Use if the platform is too sensitive.
+
Leave this application running to play animations.
+
+
+ Leave this application running to play animations, or upload
+ it to the pad.
+
+
+
+
+
+ This animation is too big to upload it to the pad:
+
+ .
+
+ However, you can leaving this application running to use it.
+ = 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 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 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();
+ }
}
}
diff --git a/smx-config/SMX.cs b/smx-config/SMX.cs
index a4942d0..60ec7be 100644
--- a/smx-config/SMX.cs
+++ b/smx-config/SMX.cs
@@ -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);
+ }
}
}