Skip to content

Instantly share code, notes, and snippets.

@GieltjE
Last active August 2, 2020 12:23
Show Gist options
  • Save GieltjE/09bd6dc9e6194860df52d9db8e8ab8e4 to your computer and use it in GitHub Desktop.
Save GieltjE/09bd6dc9e6194860df52d9db8e8ab8e4 to your computer and use it in GitHub Desktop.
C# connector for the Campzone 2019 and 2020 Badge (should be adaptable to other badge.team badges)
using System;
using System.Globalization;
using System.Text;
using System.Threading;
using SerialPortLib;
namespace CZ_Badges
{
/// <summary>
/// Sample code to make it easier to use the CZ2020 badge as programmatic input, also adds the ability to talk to the CZ2019 badge, should work under Linux too
/// Although this uses the SerialPortLib (available on nuget), this should work with most serial libraries
/// When porting to another serial library ensure that it doesn't buffer overly long (SerialPortStream does so and isn't of much use), and use baudrate 115200, Stopbits 1, Parity none, DataBits 8
/// Created by Michiel Hazelhof 2020
/// See https://docs.badge.team/ for more posibilities
/// </summary>
public class Generic
{
public enum ButtonState : Byte
{
Pressed,
Released,
Cleared,
}
public enum Buttons : Byte
{
A,
B,
Up,
Down,
Left,
Right,
}
public enum Wifi : Byte
{
Connected,
ConnectionFailed,
}
}
public class ESP32
{
internal SerialPortInput Badge;
internal Boolean BadgeInCorrectState, BadgeDataReceived;
public event Action Connected;
public event Action<Generic.Wifi, String> WifiState;
internal void OnDataReceived(String message)
{
if (!BadgeInCorrectState) return;
if (message.StartsWith("Wifi "))
{
String status = message.Substring(message.IndexOf(" ", StringComparison.Ordinal) + 1);
String messageToReport = null;
Generic.Wifi statusResult = Generic.Wifi.ConnectionFailed;
if (status.StartsWith("Connected:"))
{
statusResult = Generic.Wifi.Connected;
messageToReport = message.Substring(message.IndexOf(" ", StringComparison.Ordinal) + 1);
if (messageToReport.Contains("\r"))
{
messageToReport = message.Substring(0, messageToReport.IndexOf("\r", StringComparison.Ordinal));
}
}
Thread wifiThread = new Thread(WifiEvent);
wifiThread.Start((statusResult, messageToReport));
}
}
private void WifiEvent(Object input)
{
(Generic.Wifi state, String result) = ((Generic.Wifi, String))input;
WifiState?.Invoke(state, result);
}
internal void ConnectedEvent()
{
Connected?.Invoke();
}
/// <summary>
/// Closes the serial connection, should be called before program exit!
/// </summary>
public void Close()
{
if (Badge == null) return;
BadgeDataReceived = false;
BadgeInCorrectState = false;
if (Badge.IsConnected)
{
Badge.Disconnect();
}
Badge = null;
}
/// <summary>
/// Sets one pixel
/// </summary>
/// <param name="x">Pixel X location</param>
/// <param name="y">Pixel Y location</param>
/// <param name="rgb">Defines the color of the text in RGB, e.g. 0xFFFFFF for white</param>
public virtual Boolean DrawPixel(UInt32 x, UInt32 y, String rgb)
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import display\r\ndisplay.drawPixel(" + x.ToString(CultureInfo.InvariantCulture) + ", " + y.ToString(CultureInfo.InvariantCulture) + ", " + rgb + ")\r\ndisplay.flush()", false);
return true;
}
/// <summary>
/// Draws a line
/// </summary>
/// <param name="startX">Start pixel X location</param>
/// <param name="startY">Start pixel Y location</param>
/// <param name="endX">End pixel X location</param>
/// <param name="endY">End pixel Y location</param>
/// <param name="rgb">Defines the color of the text in RGB, e.g. 0xFFFFFF for white</param>
public virtual Boolean DrawLine(UInt32 startX, UInt32 startY, UInt32 endX, UInt32 endY, String rgb)
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import display\r\ndisplay.drawLine(" + startX.ToString(CultureInfo.InvariantCulture) + ", " + startY.ToString(CultureInfo.InvariantCulture) + ", " + endX.ToString(CultureInfo.InvariantCulture) + ", " + endY.ToString(CultureInfo.InvariantCulture) + ", " + rgb + ")\r\ndisplay.flush()", false);
return true;
}
/// <summary>
/// Draws a circle
/// </summary>
/// <param name="x0">Center X position</param>
/// <param name="y0">Center Y position</param>
/// <param name="radius">Radius of the circle</param>
/// <param name="fill">Fill the pixels within the circle</param>
/// <param name="rgb">Defines the color of the text in RGB, e.g. 0xFFFFFF for white</param>
/// <param name="radius">Radius of the circle</param>
/// <param name="radius">Radius of the circle</param>
public virtual Boolean DrawCircle(UInt32 x0, UInt32 y0, UInt32 radius, Boolean fill, String rgb, UInt32 a0, UInt32 a1)
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import display\r\ndisplay.drawCircle(" + x0.ToString(CultureInfo.InvariantCulture) + ", " + y0.ToString(CultureInfo.InvariantCulture) + ", " + radius.ToString(CultureInfo.InvariantCulture) + ", " + a0.ToString(CultureInfo.InvariantCulture) + ", " + a1.ToString(CultureInfo.InvariantCulture) + ", " + (fill ? "True" : "False") + ", " + rgb + ")\r\ndisplay.flush()", false);
return true;
}
/// <summary>
/// Displays a PNG file
/// </summary>
/// <param name="x">Top left X position</param>
/// <param name="y">Top left Y position</param>
/// <param name="file">Name of the file on the flash</param>
public virtual Boolean DrawPNG(UInt32 x, UInt32 y, String file)
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import display\r\ndisplay.drawPng(" + x.ToString(CultureInfo.InvariantCulture) + ", " + y.ToString(CultureInfo.InvariantCulture) + ", '" + file.Replace("'", "\\'") + "')\r\ndisplay.flush()", false);
return true;
}
/// <summary>
/// Displays a PNG file
/// </summary>
/// <param name="x">Top left X position</param>
/// <param name="y">Top left Y position</param>
/// <param name="data">Byte array with PNG content</param>
public virtual Boolean DrawPNG(UInt32 x, UInt32 y, Byte[] data)
{
if (!BadgeInCorrectState) return false;
StringBuilder hex = new StringBuilder(data.Length * 4);
foreach (Byte b in data)
{
hex.AppendFormat("\\x{0:x2},", b);
}
SendCustomCommand("import display\r\ndisplay.drawPng(" + x.ToString(CultureInfo.InvariantCulture) + ", " + y.ToString(CultureInfo.InvariantCulture) + ", b'" + hex.ToString().TrimEnd(',') + "')\r\ndisplay.flush()", false);
return true;
}
/// <summary>
/// Draws a line
/// </summary>
/// <param name="startX">Start pixel X location</param>
/// <param name="startY">Start pixel Y location</param>
/// <param name="width">Width in pixels</param>
/// <param name="height">Height in pixels</param>
/// <param name="rgb">Defines the color of the text in RGB, e.g. 0xFFFFFF for white</param>
/// <param name="borderOnly">Only draw the border or also fill the rectangle?</param>
public virtual Boolean DrawRectangle(UInt32 startX, UInt32 startY, UInt32 width, UInt32 height, String rgb, Boolean borderOnly)
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import display\r\ndisplay.drawRect(" + startX.ToString(CultureInfo.InvariantCulture) + ", " + startY.ToString(CultureInfo.InvariantCulture) + ", " + width.ToString(CultureInfo.InvariantCulture) + ", " + height.ToString(CultureInfo.InvariantCulture) + ", " + (borderOnly ? "False" : "True") + ", " + rgb + ")\r\ndisplay.flush()", false);
return true;
}
/// <summary>
/// Sets the background color
/// </summary>
/// <param name="rgb">Defines the color of the text in RGB, e.g. 0xFFFFFF for white</param>
public virtual Boolean SetBackGround(String rgb)
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import display\r\ndisplay.drawFill(" + rgb + ")\r\ndisplay.flush()", false);
return true;
}
/// <summary>
/// Displays a text using the new display class, defaults to white and upper left
/// </summary>
/// <param name="message">Text to display in a scrolling motion</param>
/// <param name="rgb">Defines the color of the text in RGB, e.g. 0xFFFFFF for white</param>
/// <param name="x">Defines the X portion of where to start</param>
/// <param name="y">Defines the Y portion of where to start</param>
public virtual Boolean DisplayMessage(String message, String rgb = "0xFFFFFF", Byte x = Byte.MinValue, Byte y = Byte.MinValue)
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import display\r\ndisplay.drawText(" + x.ToString(CultureInfo.InvariantCulture) + ", " + y.ToString(CultureInfo.InvariantCulture) + ", \"" + message.Replace("\"", "\\\"") + "\", " + rgb + ", \"7x5\")\r\ndisplay.flush()", false);
return true;
}
/// <summary>
/// Connects to the wifi
/// </summary>
/// <param name="ssid">Name of the network</param>
/// <param name="password">Password of the network, keep null for open</param>
public virtual Boolean ConnectToNewWifi(String ssid, String password = null)
{
if (!BadgeInCorrectState) return false;
String connection;
if (String.IsNullOrEmpty(password))
{
connection = "sta_if.connect('" + ssid.Replace("'", "\\'") + "')";
}
else
{
connection = "sta_if.connect('" + ssid.Replace("'", "\\'") + "', '" + password.Replace("'", "\\'") + "')";
}
SendCustomCommand("import network, machine, time\r\nsta_if = network.WLAN(network.STA_IF)\r\n" + connection + "\r\nwait = 50\r\nwhile not sta_if.isconnected() and wait > 0:\r\n\twait -= 1\r\n\ttime.sleep(0.1)\r\nif sta_if.isconnected():\r\n\tip_addr, netmask, gateway, dns_server = sta_if.ifconfig()\r\n\tprint(\"Wifi Connected: '{}'\".format(ip_addr))\r\nelse:\r\n\tprint(\"Wifi Failed\")", true);
return true;
}
/// <summary>
/// Connects to the wifi using the existing wifi config
/// </summary>
public virtual Boolean ConnectToWifi()
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import wifi, network\r\nwifi.connect()\r\nif not wifi.wait():\r\n\tprint(\"Wifi Failed\")\r\nelse:\r\n\tsta_if = network.WLAN(network.STA_IF)\r\n\tip_addr, netmask, gateway, dns_server = sta_if.ifconfig()\r\n\tprint(\"Wifi Connected: '{}'\".format(ip_addr))", true);
return true;
}
/// <summary>
/// Send a custom command/program/whatever you want, just ensure that you include the the proper tabbing for python
/// </summary>
/// <param name="command">Command/Program to send to the CZ2020 badge</param>
/// <param name="usePasteMode">Enter and commit paste mode automatically?</param>
/// /// <param name="includeStartingNewLine">Should only be used for the first command we send or if we don't know the current state</param>
public virtual Boolean SendCustomCommand(String command, Boolean usePasteMode, Boolean includeStartingNewLine = false, Boolean forceSend = false)
{
if (!BadgeInCorrectState && !forceSend) return false;
if (usePasteMode)
{
// Enter "paste" mode (ctrl+e)
Badge.SendMessage(new Byte[] { 5 });
}
Badge.SendMessage(Encoding.UTF8.GetBytes((includeStartingNewLine ? "\r\n" : "") + command + "\r\n"));
if (usePasteMode)
{
// Commit paste mode (ctrl+d)
Badge.SendMessage(new Byte[] { 4 });
Badge.SendMessage(Encoding.UTF8.GetBytes("\r\n"));
}
return true;
}
}
public class CZ2019 : ESP32
{
private Boolean _badgeLoadProgramOnConnect;
public event Action<Generic.ButtonState, Generic.Buttons> ButtonEvent;
/// <summary>
/// Opens a serial connection to the CZ2019 badge
/// </summary>
/// <param name="comPort">A string containing the name of the port to connect to, e.g. /dev/ttyUSB0 for Linux or COM1 for windows</param>
/// <param name="loadProgramOnConnect">If true we load the button program on the connect event so we can listen for button events</param>
public void Open(String comPort, Boolean loadProgramOnConnect)
{
if (Badge != null && Badge.IsConnected)
{
Badge.Disconnect();
}
_badgeLoadProgramOnConnect = loadProgramOnConnect;
Badge = new SerialPortInput();
Badge.SetPort(comPort);
Badge.MessageReceived += OnDataReceived;
Badge.ConnectionStatusChanged += BadgeOnConnectionStatusChanged;
Badge.Connect();
}
/// <summary>
/// Closes the serial connection, should be called before program exit!
/// </summary>
private void BadgeOnConnectionStatusChanged(Object sender, ConnectionStatusChangedEventArgs args)
{
if (!args.Connected)
{
BadgeDataReceived = false;
BadgeInCorrectState = false;
}
else
{
// Sometimes the badge doesn't send any data, initiate it with an enter stroke
Thread.Sleep(2000);
if (!BadgeDataReceived)
{
Badge.SendMessage(Encoding.UTF8.GetBytes("\r\n"));
}
}
}
private void OnDataReceived(Object sender, MessageReceivedEventArgs args)
{
BadgeDataReceived = true;
String message = Encoding.Default.GetString(args.Data);
// We should *NOT* hold up the DataReceived event as that would stop us from receiving new data until the event has finished!
if (BadgeInCorrectState)
{
if (message.StartsWith("Pressed ", StringComparison.Ordinal))
{
Thread helperThread = new Thread(BadgeButtonEventHelper);
helperThread.Start((Generic.ButtonState.Pressed, (Generic.Buttons)Enum.Parse(typeof(Generic.Buttons), message.Substring(message.IndexOf(" ", StringComparison.Ordinal) + 1))));
}
else if (message.StartsWith("Released ", StringComparison.Ordinal))
{
Thread helperThread = new Thread(BadgeButtonEventHelper);
helperThread.Start((Generic.ButtonState.Released, (Generic.Buttons)Enum.Parse(typeof(Generic.Buttons), message.Substring(message.IndexOf(" ", StringComparison.Ordinal) + 1))));
}
}
else
{
// Detect and escape the menu
if (message.Contains("[0;37;40m Settings"))
{
// Send ctrl+c to escape
//_badge.SendMessage(new Byte[] { 3 });
// Assume that we are aligned on the menu with the python terminal
Badge.SendMessage(Encoding.UTF8.GetBytes("\r\n"));
}
else if (message.EndsWith(">>> ", StringComparison.Ordinal))
{
BadgeInCorrectState = true;
Thread helperThread = new Thread(BadgeConnectedHelper);
helperThread.Start();
}
}
OnDataReceived(message);
}
private void BadgeConnectedHelper()
{
if (_badgeLoadProgramOnConnect)
{
// Simple program that enables us to listen to keypresses
SendCustomCommand("import buttons, defines\r\ndef btn_down(down):\r\n\tif down:\r\n\t\tprint(\"Pressed Down\")\r\n\telse:\r\n\t\tprint(\"Released Down\")\r\ndef btn_left(down):\r\n\tif down:\r\n\t\tprint(\"Pressed Left\")\r\n\telse:\r\n\t\tprint(\"Released Left\")\r\ndef btn_right(down):\r\n\tif down:\r\n\t\tprint(\"Pressed Right\")\r\n\telse:\r\n\t\tprint(\"Released Right\")\r\ndef btn_up(down):\r\n\tif down:\r\n\t\tprint(\"Pressed Up\")\r\n\telse:\r\n\t\tprint(\"Released Up\")\r\ndef btn_a(down):\r\n\tif down:\r\n\t\tprint(\"Pressed A\")\r\n\telse:\r\n\t\tprint(\"Released A\")\r\ndef btn_b(down):\r\n\tif down:\r\n\t\tprint(\"Pressed B\")\r\n\telse:\r\n\t\tprint(\"Released B\")\r\n\t\t\r\nbuttons.register(defines.BTN_DOWN, btn_down)\r\nbuttons.register(defines.BTN_LEFT, btn_left)\r\nbuttons.register(defines.BTN_RIGHT, btn_right)\r\nbuttons.register(defines.BTN_UP, btn_up)\r\nbuttons.register(defines.BTN_A, btn_a)\r\nbuttons.register(defines.BTN_B, btn_b)", true, true);
}
else
{
// Send a couple of enters to ensure we are in a stable state
SendCustomCommand("", false, true);
}
ConnectedEvent();
}
private void BadgeButtonEventHelper(Object buttonInformation)
{
(Generic.ButtonState action, Generic.Buttons button) = ((Generic.ButtonState, Generic.Buttons))buttonInformation;
ButtonEvent?.Invoke(action, button);
}
/// <summary>
/// Displays a text using the old RGB class, defaults to white and upper left, use this one for the CZ2019 badge as it should be faster
/// </summary>
/// <param name="message">Text to display in a scrolling motion</param>
/// <param name="r">Defines the R portion of the RGB color range</param>
/// <param name="g">Defines the G portion of the RGB color range</param>
/// <param name="b">Defines the B portion of the RGB color range</param>
/// <param name="x">Defines the X portion of where to start</param>
/// <param name="y">Defines the Y portion of where to start</param>
public Boolean DisplayMessage(String message, Byte r = Byte.MaxValue, Byte g = Byte.MaxValue, Byte b = Byte.MaxValue, Byte x = Byte.MinValue, Byte y = Byte.MinValue)
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import rgb\r\nrgb.text(\"" + message.Replace("\"", "\\\"") + "\", (" + r.ToString(CultureInfo.InvariantCulture) + "," + g.ToString(CultureInfo.InvariantCulture) + "," + b.ToString(CultureInfo.InvariantCulture) + "), (" + x.ToString(CultureInfo.InvariantCulture) + ", " + y.ToString(CultureInfo.InvariantCulture) + "))", false);
return true;
}
/// <summary>
/// Sets the background color to the specified RGB range, defaults to black
/// </summary>
/// <param name="r">Defines the R portion of the RGB color range</param>
/// <param name="g">Defines the G portion of the RGB color range</param>
/// <param name="b">Defines the B portion of the RGB color range</param>
public Boolean SetBackground(Byte r = Byte.MinValue, Byte g = Byte.MinValue, Byte b = Byte.MinValue)
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import rgb\r\nrgb.background((" + r.ToString(CultureInfo.InvariantCulture) + "," + g.ToString(CultureInfo.InvariantCulture) + "," + b.ToString(CultureInfo.InvariantCulture) + "))", false);
return true;
}
/// <summary>
/// Scrolls a text, defaults to white and upper left
/// </summary>
/// <param name="message">Text to display in a scrolling motion</param>
/// <param name="r">Defines the R portion of the RGB color range</param>
/// <param name="g">Defines the G portion of the RGB color range</param>
/// <param name="b">Defines the B portion of the RGB color range</param>
/// <param name="x">Defines the X portion of where to start</param>
/// <param name="y">Defines the Y portion of where to start</param>
public Boolean ScrollMessage(String message, Byte r = Byte.MaxValue, Byte g = Byte.MaxValue, Byte b = Byte.MaxValue, Byte x = Byte.MinValue, Byte y = Byte.MinValue)
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import rgb\r\nrgb.scrolltext(\"" + message.Replace("\"", "\\\"") + "\", (" + r.ToString(CultureInfo.InvariantCulture) + "," + g.ToString(CultureInfo.InvariantCulture) + "," + b.ToString(CultureInfo.InvariantCulture) + "), (" + x.ToString(CultureInfo.InvariantCulture) + ", " + y.ToString(CultureInfo.InvariantCulture) + "))", false);
return true;
}
/// <summary>
/// Sets the refresh rate, a higher number means faster animation/scrolling
/// </summary>
/// <param name="frameRate">0-30</param>
public Boolean SetFrameRate(Byte frameRate)
{
if (!BadgeInCorrectState) return false;
if (frameRate > 30)
{
throw new Exception("Max frameRate is 30!");
}
SendCustomCommand("import rgb\r\nrgb.frameRate(" + frameRate.ToString(CultureInfo.InvariantCulture) + ")", false);
return true;
}
/// <summary>
/// Set to rgb.FONT_7x5 for the 7x5 monospace font and rgb.FONT_6x3 for 6x3 proportional font
/// </summary>
/// <param name="font">font to use</param>
public Boolean SetFont(String font)
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import rgb\r\nrgb.setfont(" + font + ")", false);
return true;
}
/// <summary>
/// Sets the brightness of the badge
/// </summary>
/// <param name="brightness">0-30</param>
public Boolean SetBrightness(Byte brightness)
{
if (!BadgeInCorrectState) return false;
if (brightness > 30)
{
throw new Exception("Max brightness is 30!");
}
SendCustomCommand("import rgb\r\nrgb.setbrightness(" + brightness.ToString(CultureInfo.InvariantCulture) + ")", false);
return true;
}
/// <summary>
/// Clears the display
/// </summary>
public Boolean ClearDisplay()
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import rgb\r\nrgb.clear()", false);
return true;
}
}
public class CZ2020 : ESP32
{
private Boolean _badgeLoadProgramOnConnect, _badgeLoadProgramSingle, _badgeShellStarted;
public event Action<Generic.ButtonState, Byte> ButtonEvent;
private Int32 _audioChannel = -1;
/// <summary>
/// Opens a serial connection to the CZ2020 badge
/// </summary>
/// <param name="comPort">A string containing the name of the port to connect to, e.g. /dev/ttyUSB0 for Linux or COM1 for windows</param>
/// <param name="loadProgramOnConnect">If true we load the button program on the connect event so we can listen for button events</param>
/// <param name="loadProgramSingleMode">If true only one button will be active, if false all buttons preserve their own state</param>
public void Open(String comPort, Boolean loadProgramOnConnect, Boolean loadProgramSingleMode)
{
if (Badge != null && Badge.IsConnected)
{
Badge.Disconnect();
}
_badgeShellStarted = false;
_badgeLoadProgramOnConnect = loadProgramOnConnect;
_badgeLoadProgramSingle = loadProgramSingleMode;
Badge = new SerialPortInput();
Badge.SetPort(comPort);
Badge.MessageReceived += OnDataReceived;
Badge.ConnectionStatusChanged += BadgeOnConnectionStatusChanged;
Badge.Connect();
}
private void BadgeOnConnectionStatusChanged(Object sender, ConnectionStatusChangedEventArgs args)
{
if (!args.Connected)
{
BadgeDataReceived = false;
BadgeInCorrectState = false;
_badgeShellStarted = false;
}
else
{
// Sometimes the badge doesn't send any data, initiate it with an enter stroke
Thread.Sleep(2000);
if (!BadgeDataReceived)
{
Badge.SendMessage(Encoding.UTF8.GetBytes("\r\n"));
}
}
}
private void OnDataReceived(Object sender, MessageReceivedEventArgs args)
{
String message = Encoding.Default.GetString(args.Data);
BadgeDataReceived = true;
// We should *NOT* hold up the DataReceived event as that would stop us from receiving new data until the event has finished!
if (BadgeInCorrectState)
{
if (message.StartsWith("Pressed ", StringComparison.Ordinal))
{
Thread helperThread = new Thread(BadgeButtonEventHelper);
helperThread.Start((Generic.ButtonState.Pressed, Convert.ToByte(message.Substring(message.IndexOf(" ", StringComparison.Ordinal) + 1))));
}
else if (message.StartsWith("Released ", StringComparison.Ordinal))
{
Thread helperThread = new Thread(BadgeButtonEventHelper);
helperThread.Start((Generic.ButtonState.Released, Convert.ToByte(message.Substring(message.IndexOf(" ", StringComparison.Ordinal) + 1))));
}
else if (message.StartsWith("Cleared ", StringComparison.Ordinal))
{
Thread helperThread = new Thread(BadgeButtonEventHelper);
helperThread.Start((Generic.ButtonState.Cleared, Convert.ToByte(message.Substring(message.IndexOf(" ", StringComparison.Ordinal) + 1))));
}
else if (message.StartsWith("Channel ", StringComparison.Ordinal))
{
_audioChannel = Convert.ToInt32(message.Substring(message.IndexOf(" ", StringComparison.Ordinal) + 1));
}
}
// If we have not started the app, but have detected a console entry line, start it ;)
else if (message.EndsWith(">>> ", StringComparison.Ordinal))
{
if (_badgeLoadProgramOnConnect && !_badgeShellStarted)
{
SendCustomCommand("import system\r\nsystem.start('shell')", false, true, true);
_badgeShellStarted = true;
}
else
{
BadgeInCorrectState = true;
Thread helperThread = new Thread(BadgeConnectedHelper);
helperThread.Start();
}
}
OnDataReceived(message);
}
private void BadgeConnectedHelper()
{
if (_badgeLoadProgramOnConnect)
{
// Simple program that enables us to listen to keypresses
SendCustomCommand("import keypad, display\r\n\r\ndisplay.drawFill(0x000000)\r\ndisplay.flush()\r\n\r\ndef on_key(key_index, pressed):\r\n\tglobal selectedX, selectedY, selectedKeys, singleMode\r\n\tx, y = key_index % 4, int(key_index / 4)\r\n\tcurrentKeySelected = False\r\n\tif (singleMode):\r\n\t\tif (x == selectedX and y == selectedY):\r\n\t\t\tcurrentKeySelected = True\r\n\telse:\r\n\t\tif (key_index in selectedKeys):\r\n\t\t\tcurrentKeySelected = True\r\n\t\t\r\n\tif (pressed and currentKeySelected):\r\n\t\tprint('Cleared', key_index)\r\n\t\tdisplay.flush()\r\n\t\tif (singleMode):\r\n\t\t\tselectedX = -1\r\n\t\t\tselectedY = -1\r\n\t\telse:\r\n\t\t\tselectedKeys.remove(key_index)\r\n\t\tdisplay.drawPixel(x, y, 0x000000)\r\n\t\tdisplay.flush()\r\n\t\treturn\r\n\tif pressed:\r\n\t\tprint('Pressed', key_index)\r\n\t\tif (singleMode):\r\n\t\t\tdisplay.drawFill(0x000000)\r\n\t\t\tselectedX = x\r\n\t\t\tselectedY = y\r\n\t\telse:\r\n\t\t\tselectedKeys.add(key_index)\r\n\t\tdisplay.drawPixel(x, y, 0xF8B700)\r\n\t\tdisplay.flush()\r\n\telif (currentKeySelected):\r\n\t\t print('Released', key_index)\r\n\r\nkeypad.add_handler(on_key)\r\nselectedX = -1\r\nselectedY = -1\r\nselectedKeys = set()\r\nsingleMode = " + (_badgeLoadProgramSingle ? "True" : "False"), true, true);
}
else
{
// Send a couple of enters to ensure we are in a stable state
SendCustomCommand("", false, true);
}
ConnectedEvent();
}
private void BadgeButtonEventHelper(Object buttonInformation)
{
(Generic.ButtonState action, Byte button) = ((Generic.ButtonState, Byte))buttonInformation;
ButtonEvent?.Invoke(action, button);
}
/// <summary>
/// Speaks the text using the build in text2speech, wifi is required
/// </summary>
/// <param name="text">Text to speak</param>
/// <param name="language">Text to speak</param>
/// <param name="volume">Volume 0-255</param>
/// <param name="fileNameToSave">Filename to save to, saved to: /cache/fileNameToSave.mp3</param>
public Boolean SpeakText(String text, String language, Byte volume, String fileNameToSave = null)
{
if (!BadgeInCorrectState) return false;
String save = null;
if (!String.IsNullOrEmpty(fileNameToSave))
{
save = "\r\nugTTS.text_to_mp3('" + text.Replace("'", "\\'") + "', '/cache/" + fileNameToSave + ".mp3')";
}
SendCustomCommand("import wifi, ugTTS\r\nwifi.connect()\r\nif not wifi.wait():\r\n\tprint('Wifi Failed')\r\nelse:\r\n\tugTTS.speak('" + text.Replace("'", "\\'") + "', lang='" + language + "', volume=" + volume.ToString(CultureInfo.InvariantCulture) + ")" + save, true);
return true;
}
/// <summary>
/// Plays the requested file
/// </summary>
/// <param name="fileOrURI">Full path on flash or URI (e.g. podcast/stream)</param>
/// <param name="volume">Volume 0-255</param>
/// <param name="loop">Loop the file</param>
/// <param name="bpm">BPM of the music, e.g. 120</param>
/// <param name="startAtNext">Start at the next (1-32) to start playback at the next x-th 8th note (example: 1 starts at next 8th, 2 at next 4th (namely 2x an 8th), 4 at half note, 8 at whole note, 32 at whole bar)</param>
/// <param name="onFinished">If on_finished is a function, it is called when the playback ends.</param>
public (Boolean succes, Int32 channel) PlayAudio(String fileOrURI, Byte volume, Boolean loop, UInt16 bpm = 0, Byte startAtNext = 0, String onFinished = null)
{
if (!BadgeInCorrectState) return (false, -1);
String extra = "";
if (loop)
{
extra += ", loop=True";
}
if (bpm > 0)
{
extra += ", sync_beat=" + bpm.ToString(CultureInfo.InvariantCulture);
}
if (startAtNext > 0)
{
extra += ", start_at_next=" + startAtNext.ToString(CultureInfo.InvariantCulture);
}
if (!String.IsNullOrEmpty(onFinished))
{
extra += ", on_finished=" + onFinished;
}
_audioChannel = -1;
SendCustomCommand("import audio\r\nchannel_id = audio.play('" + fileOrURI + "', volume=" + volume.ToString(CultureInfo.InvariantCulture) + extra + ")\r\nprint('Channel', channel_id)", false);
Byte timeOut = 0;
while (_audioChannel == -1)
{
Thread.Sleep(50);
if (timeOut++ > 20)
{
break;
}
}
return (true, _audioChannel);
}
/// <summary>
/// Stops all playback on an audio channel
/// </summary>
/// <param name="channel">Channel as received from PlayAudio()</param>
public Boolean StopAudio(Int32 channel)
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import audio\r\nstop_channel(" + channel.ToString(CultureInfo.InvariantCulture) + ")", false);
return true;
}
/// <summary>
/// Stops the loop on an audio channel
/// </summary>
/// <param name="channel">Channel as received from PlayAudio()</param>
public Boolean StopAudioLoop(Int32 channel)
{
if (!BadgeInCorrectState) return false;
SendCustomCommand("import audio\r\nstop_looping(" + channel.ToString(CultureInfo.InvariantCulture) + ")", false);
return true;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment