Last active
August 2, 2020 12:23
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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