// Helpers of the programmer GUI: USB device discovery, running the // command-line tools, settings, a few Win32 calls. using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Drawing; using System.IO; using System.Management; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using Microsoft.Win32.SafeHandles; namespace TangProgrammer { // What Windows sees on USB (see README: installation and restore) sealed class BridgeState { public bool Present; // this firmware: DirtyJTAG + CDC UART, 1209:C0CA public string Version; // from bcdDevice, e.g. "1.00" public string UartPort; // COM port of its CDC interface public bool JtagDriverOk = true; public string BootloaderPort; // BL616 ROM USB bootloader "Bouffalo CDC DEMO", 349B:6160 public bool SipeedStock; // stock Sipeed firmware emulates an FT2232, 0403:6010 } static class Devices { static readonly Regex ComRx = new Regex(@"\((COM\d+)\)"); static readonly Regex RevRx = new Regex(@"REV_(\d\d)(\d\d)"); public static BridgeState Scan() { var st = new BridgeState(); const string query = "SELECT Name, PNPDeviceID, HardwareID, ConfigManagerErrorCode " + "FROM Win32_PnPEntity WHERE PNPDeviceID LIKE 'USB%'"; using (var searcher = new ManagementObjectSearcher(query)) using (ManagementObjectCollection found = searcher.Get()) { foreach (ManagementBaseObject o in found) { using (o) { string id = Convert.ToString(o["PNPDeviceID"]).ToUpperInvariant(); Match com = ComRx.Match(Convert.ToString(o["Name"])); if (id.Contains("VID_1209&PID_C0CA")) { st.Present = true; if (id.Contains("&MI_00")) st.JtagDriverOk = Convert.ToUInt32(o["ConfigManagerErrorCode"]) == 0; else if (com.Success) st.UartPort = com.Groups[1].Value; var hwIds = o["HardwareID"] as string[]; if (st.Version == null && hwIds != null) { foreach (string hw in hwIds) { Match m = RevRx.Match(hw.ToUpperInvariant()); if (m.Success) { st.Version = int.Parse(m.Groups[1].Value) + "." + m.Groups[2].Value; break; } } } } else if (id.Contains("VID_349B&PID_6160")) { if (com.Success) st.BootloaderPort = com.Groups[1].Value; } else if (id.Contains("VID_0403&PID_6010")) { st.SipeedStock = true; } } } } return st; } } // A drive mapped to a share of this very PC (D: -> \\THISPC\Projects) sends // every tool launch through SMB; use the folder behind the share instead, // like Resolve-LocalPath in scripts\common.ps1. static class LocalPath { [DllImport("mpr.dll", CharSet = CharSet.Unicode)] static extern int WNetGetConnection(string localName, StringBuilder remoteName, ref int length); public static string Resolve(string path) { string full = Path.GetFullPath(path); try { if (full.Length < 3 || full[1] != ':') return full; var unc = new StringBuilder(512); int len = unc.Capacity; if (WNetGetConnection(full.Substring(0, 2), unc, ref len) != 0) return full; Match m = Regex.Match(unc.ToString(), @"^\\\\([^\\]+)\\([^\\]+)\\?(.*)$"); if (!m.Success) return full; string host = m.Groups[1].Value; if (!host.Equals(Environment.MachineName, StringComparison.OrdinalIgnoreCase) && !host.Equals("localhost", StringComparison.OrdinalIgnoreCase) && host != "127.0.0.1") return full; string query = "SELECT Path FROM Win32_Share WHERE Name='" + m.Groups[2].Value.Replace("'", "\\'") + "'"; using (var searcher = new ManagementObjectSearcher(query)) using (ManagementObjectCollection found = searcher.Get()) { foreach (ManagementBaseObject o in found) { string share = Convert.ToString(o["Path"]); if (share.Length == 0) continue; string local = Path.Combine(Path.Combine(share, m.Groups[3].Value), full.Substring(3)); if (Directory.Exists(local) || File.Exists(local)) return local; } } } catch (Exception) { } return full; } } // Runs a console tool without a console window and streams its output // (stdout and stderr chunks, as they arrive) to a callback on a worker thread. static class ToolProcess { public static Task Run(string exe, string args, string workDir, Action output, CancellationToken ct) { var psi = new ProcessStartInfo(exe, args) { UseShellExecute = false, CreateNoWindow = true, RedirectStandardOutput = true, RedirectStandardError = true, StandardOutputEncoding = Encoding.Default, StandardErrorEncoding = Encoding.Default, WorkingDirectory = workDir ?? Path.GetDirectoryName(exe), }; Process p = Process.Start(psi); Thread outPump = Pump(p.StandardOutput, false, output); Thread errPump = Pump(p.StandardError, true, output); return Task.Run(() => { using (p) using (ct.Register(() => Task.Run(() => KillTree(p)))) { p.WaitForExit(); outPump.Join(); errPump.Join(); return p.ExitCode; } }); } static Thread Pump(StreamReader reader, bool isErr, Action output) { var t = new Thread(() => { var buf = new char[4096]; try { int n; while ((n = reader.Read(buf, 0, buf.Length)) > 0) output(new string(buf, 0, n), isErr); } catch (Exception) { } }); t.IsBackground = true; t.Start(); return t; } // BLFlashCommand is a one-file PyInstaller exe: the real work happens in a child static void KillTree(Process p) { try { if (p.HasExited) return; var psi = new ProcessStartInfo("taskkill.exe", "/T /F /PID " + p.Id) { UseShellExecute = false, CreateNoWindow = true, }; using (Process k = Process.Start(psi)) k.WaitForExit(5000); if (!p.HasExited) p.Kill(); } catch (Exception) { } } } // Splits console output into lines. A lone '\r' also ends a line: that is // how progress bars redraw themselves. sealed class LineSplitter { readonly StringBuilder cur = new StringBuilder(); public List Feed(string chunk) { var lines = new List(); foreach (char ch in chunk) { if (ch == '\r' || ch == '\n') { if (cur.Length > 0) lines.Add(cur.ToString()); cur.Clear(); } else { cur.Append(ch); } } return lines; } public string Partial { get { return cur.ToString(); } } public string Flush() { string rest = cur.ToString(); cur.Clear(); return rest; } } sealed class Settings { public readonly List Recent = new List(); public int FreqMHz = 6; public bool Flash; public bool Verify = true; public bool AutoReload; public int Tab; public string Theme = "system"; // system | light | dark public Rectangle Bounds; public bool Maximized; static string FilePath { get { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Tang25kProgrammer", "settings.ini"); } } public static Settings Load() { var s = new Settings(); try { if (!File.Exists(FilePath)) return s; foreach (string line in File.ReadAllLines(FilePath, Encoding.UTF8)) { int eq = line.IndexOf('='); if (eq <= 0) continue; string key = line.Substring(0, eq).Trim(); string val = line.Substring(eq + 1).Trim(); switch (key) { case "recent": if (val.Length > 0) s.Recent.Add(val); break; case "freq": int.TryParse(val, out s.FreqMHz); break; case "flash": s.Flash = val == "1"; break; case "verify": s.Verify = val == "1"; break; case "auto": s.AutoReload = val == "1"; break; case "tab": int.TryParse(val, out s.Tab); break; case "theme": if (val == "light" || val == "dark" || val == "system") s.Theme = val; break; case "maximized": s.Maximized = val == "1"; break; case "bounds": string[] p = val.Split(','); if (p.Length == 4) s.Bounds = new Rectangle(int.Parse(p[0]), int.Parse(p[1]), int.Parse(p[2]), int.Parse(p[3])); break; } } } catch (Exception) { } return s; } public void Save() { try { var sb = new StringBuilder(); foreach (string r in Recent) sb.AppendLine("recent=" + r); sb.AppendLine("freq=" + FreqMHz); sb.AppendLine("flash=" + (Flash ? 1 : 0)); sb.AppendLine("verify=" + (Verify ? 1 : 0)); sb.AppendLine("auto=" + (AutoReload ? 1 : 0)); sb.AppendLine("tab=" + Tab); sb.AppendLine("theme=" + Theme); sb.AppendLine("maximized=" + (Maximized ? 1 : 0)); if (!Bounds.IsEmpty) sb.AppendLine("bounds=" + Bounds.X + "," + Bounds.Y + "," + Bounds.Width + "," + Bounds.Height); Directory.CreateDirectory(Path.GetDirectoryName(FilePath)); File.WriteAllText(FilePath, sb.ToString(), Encoding.UTF8); } catch (Exception) { } } } static class Native { [StructLayout(LayoutKind.Sequential)] struct FLASHWINFO { public uint cbSize; public IntPtr hwnd; public uint dwFlags; public uint uCount; public uint dwTimeout; } [DllImport("user32.dll")] static extern bool FlashWindowEx(ref FLASHWINFO fi); // blink the taskbar button until the window is activated public static void FlashTaskbar(Form f) { var fi = new FLASHWINFO { cbSize = (uint)Marshal.SizeOf(typeof(FLASHWINFO)), hwnd = f.Handle, dwFlags = 3 /* FLASHW_ALL */ | 12 /* FLASHW_TIMERNOFG */, }; FlashWindowEx(ref fi); } [StructLayout(LayoutKind.Sequential)] struct DCB { public int DCBlength; public uint BaudRate; public uint Flags; public ushort wReserved; public ushort XonLim; public ushort XoffLim; public byte ByteSize; public byte Parity; public byte StopBits; public byte XonChar; public byte XoffChar; public byte ErrorChar; public byte EofChar; public byte EvtChar; public ushort wReserved1; } [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern SafeFileHandle CreateFile(string name, uint access, uint share, IntPtr security, uint disposition, uint flags, IntPtr template); [DllImport("kernel32.dll", SetLastError = true)] static extern bool GetCommState(SafeFileHandle h, ref DCB dcb); [DllImport("kernel32.dll", SetLastError = true)] static extern bool SetCommState(SafeFileHandle h, ref DCB dcb); // Opens the serial port, sets the baud rate and closes it again. The // bridge firmware reboots into the ROM bootloader on 1234 baud, so the // device vanishes while the port is open: System.IO.Ports does not // survive that, plain handles do. Returns null or the error text. public static string PokeBaudRate(string port, uint baud) { using (SafeFileHandle h = CreateFile(@"\\.\" + port, 0xC0000000 /* GENERIC_READ | GENERIC_WRITE */, 0, IntPtr.Zero, 3 /* OPEN_EXISTING */, 0, IntPtr.Zero)) { if (h.IsInvalid) return new Win32Exception(Marshal.GetLastWin32Error()).Message; var dcb = new DCB(); dcb.DCBlength = Marshal.SizeOf(typeof(DCB)); if (!GetCommState(h, ref dcb)) return new Win32Exception(Marshal.GetLastWin32Error()).Message; dcb.BaudRate = baud; if (dcb.ByteSize == 0) dcb.ByteSize = 8; // may report an error: the device can be gone before the call returns SetCommState(h, ref dcb); Thread.Sleep(200); } return null; } } }