/// <summary> /// Returns a list of available serial ports with their associated PID, VID and descriptions /// Modified from the example written by Kamil Górski (freakone) available at /// http://blog.gorski.pm/serial-port-details-in-c-sharp /// https://github.com/freakone/serial-reader /// Some modifications were based on this stackoverflow thread: /// https://stackoverflow.com/questions/11458835/finding-information-about-all-serial-devices-connected-through-usb-in-c-sharp /// </summary> /// <returns>List of available serial ports</returns> private static List <ComPort> GetSerialPorts() { const string vidPattern = @"VID_([0-9A-F]{4})"; const string pidPattern = @"PID_([0-9A-F]{4})"; const string namePattern = @"(?<=\()COM[0-9]{1,3}(?=\)$)"; using (var searcher = new ManagementObjectSearcher("root\\CIMV2", "SELECT * FROM Win32_PnPEntity WHERE ClassGuid=\"{4d36e978-e325-11ce-bfc1-08002be10318}\"")) { var ports = searcher.Get().Cast <ManagementBaseObject>().ToList(); List <ComPort> detectedPorts = ports.Select(p => { ComPort c = new ComPort(); c.name = p.GetPropertyValue("Name").ToString(); c.vid = p.GetPropertyValue("PNPDeviceID").ToString(); c.description = p.GetPropertyValue("Caption").ToString(); Match mVID = Regex.Match(c.vid, vidPattern, RegexOptions.IgnoreCase); Match mPID = Regex.Match(c.vid, pidPattern, RegexOptions.IgnoreCase); if (mVID.Success) { c.vid = mVID.Groups[1].Value; c.vid = c.vid.Substring(0, Math.Min(4, c.vid.Length)); } else { c.vid = "----"; } if (mPID.Success) { c.pid = mPID.Groups[1].Value; c.pid = c.pid.Substring(0, Math.Min(4, c.vid.Length)); } else { c.pid = "----"; } Match mName = Regex.Match(c.name, namePattern); if (mName.Success) { c.name = mName.Value; c.num = int.Parse(c.name.Substring(3)); if (c.num == 0) { c.name = "?"; } } else { c.name = "?"; c.num = 0; } c.board = MatchBoard(c.vid, c.pid); return(c); }).ToList(); // remove all unusable ports from the list detectedPorts.RemoveAll(p => p.name == "?"); return(detectedPorts); } }
/// <summary> /// Returns a list of available serial ports with their associated PID, VID and descriptions /// Modified from the example written by Kamil Górski (freakone) available at /// http://blog.gorski.pm/serial-port-details-in-c-sharp /// https://github.com/freakone/serial-reader /// Some modifications were based on this stackoverflow thread: /// https://stackoverflow.com/questions/11458835/finding-information-about-all-serial-devices-connected-through-usb-in-c-sharp /// Hardware Bus Description through WMI is based on Simon Mourier's answer on this stackoverflow thread: /// https://stackoverflow.com/questions/69362886/get-devpkey-device-busreporteddevicedesc-from-win32-pnpentity-in-c-sharp /// </summary> /// <returns>List of available serial ports</returns> private static List <ComPort> GetSerialPorts() { const string vidPattern = @"VID_([0-9A-F]{4})"; const string pidPattern = @"PID_([0-9A-F]{4})"; const string namePattern = @"(?<=\()COM[0-9]{1,3}(?=\)$)"; const string query = "SELECT * FROM Win32_PnPEntity WHERE ClassGuid=\"{4d36e978-e325-11ce-bfc1-08002be10318}\""; // as per INTERFACE_PREFIXES in adafruit_board_toolkit // (see https://github.com/adafruit/Adafruit_Board_Toolkit/blob/main/adafruit_board_toolkit) string[] cpb_descriptions = new string[] { "CircuitPython CDC ", "Sol CDC ", "StringCarM0Ex CDC " }; List <ComPort> detectedPorts = new List <ComPort>(); foreach (var p in new ManagementObjectSearcher("root\\CIMV2", query).Get().OfType <ManagementObject>()) { ComPort c = new ComPort(); // extract and clean up port name and number c.name = p.GetPropertyValue("Name").ToString(); Match mName = Regex.Match(c.name, namePattern); if (mName.Success) { c.name = mName.Value; c.num = int.Parse(c.name.Substring(3)); } // if the port name or number cannot be determined, skip this port and move on if (c.num < 1) { continue; } // get the device's VID and PID string pidvid = p.GetPropertyValue("PNPDeviceID").ToString(); // extract and clean up device's VID Match mVID = Regex.Match(pidvid, vidPattern, RegexOptions.IgnoreCase); if (mVID.Success) { c.vid = mVID.Groups[1].Value.Substring(0, Math.Min(4, c.vid.Length)); } // extract and clean up device's PID Match mPID = Regex.Match(pidvid, pidPattern, RegexOptions.IgnoreCase); if (mPID.Success) { c.pid = mPID.Groups[1].Value.Substring(0, Math.Min(4, c.pid.Length)); } // extract the device's friendly description (caption) c.description = p.GetPropertyValue("Caption").ToString(); // attempt to match this device with a known board c.board = MatchBoard(c.vid, c.pid); // extract the device's hardware bus description c.busDescription = ""; var inParams = new object[] { new string[] { "DEVPKEY_Device_BusReportedDeviceDesc" }, null }; p.InvokeMethod("GetDeviceProperties", inParams); var outParams = (ManagementBaseObject[])inParams[1]; if (outParams.Length > 0) { var data = outParams[0].Properties.OfType <PropertyData>().FirstOrDefault(d => d.Name == "Data"); if (data != null) { c.busDescription = data.Value.ToString(); } } // we can determine if this is a CircuitPython board by its bus description foreach (string prefix in cpb_descriptions) { if (c.busDescription.StartsWith(prefix)) { c.isCircuitPython = true; } } // add this port to our list of detected ports detectedPorts.Add(c); } return(detectedPorts); }
static void Main(string[] args) { // process all command-line arguments ProcessArguments(args); // set up keyboard input for program control / relay to serial port ConsoleKeyInfo keyInfo = new ConsoleKeyInfo(); Console.TreatControlCAsInput = true; // we need to use CTRL-C to activate the REPL in CircuitPython, so it can't be used to exit the application // this is where data read from the serial port will be temporarily stored string received = string.Empty; //main loop - keep this up until user presses CTRL-X or an exception takes us down do { // first things first, check for (and respect) a request to exit the program via CTRL-X if (Console.KeyAvailable) { keyInfo = Console.ReadKey(intercept: true); if ((keyInfo.Key == ConsoleKey.X) && (keyInfo.Modifiers == ConsoleModifiers.Control)) { Output("\n<<< SimplySerial session terminated via CTRL-X >>>"); ExitProgram(silent: true); } } // get a list of available ports availablePorts = (SimplySerial.GetSerialPorts()).OrderBy(p => p.num).ToList(); // if no port was specified/selected, pick one automatically if (port.name == String.Empty) { // if there are com ports available, pick one if (availablePorts.Count() >= 1) { // first, try to default to something that we assume is running CircuitPython SimplySerial.port = availablePorts.Find(p => p.board.isCircuitPython == true); // if that doesn't work out, just default to the first available COM port if (SimplySerial.port.name == null) { SimplySerial.port = availablePorts[0]; } } // if there are no com ports available, exit or try again depending on autoconnect setting else { if (autoConnect == AutoConnect.NONE) { ExitProgram("No COM ports detected.", exitCode: -1); } else { continue; } } } // if a specific port has been selected, try to match it with one that actually exists else { bool portMatched = false; foreach (ComPort p in availablePorts) { if (p.name == port.name) { portMatched = true; port = p; break; } } // if the specified port is not available, exit or try again depending on autoconnect setting if (!portMatched) { if (autoConnect == AutoConnect.NONE) { ExitProgram(("Invalid port specified <" + port.name + ">"), exitCode: -1); } else { continue; } } } // if we get this far, it should be safe to set up the specified/selected serial port serialPort = new SerialPort(port.name, baud, parity, dataBits, stopBits) { Handshake = Handshake.None, // we don't need to support any handshaking at this point ReadTimeout = 1, // minimal timeout - we don't want to wait forever for data that may not be coming! WriteTimeout = 250, // small delay - if we go too small on this it causes System.IO semaphore timeout exceptions DtrEnable = true, // without this we don't ever receive any data RtsEnable = true // without this we don't ever receive any data }; // attempt to open the serial port, deal with failures try { serialPort.Open(); } catch (Exception e) { // if auto-connect is disabled than any exception should result in program termination if (autoConnect == AutoConnect.NONE) { if (e is UnauthorizedAccessException) { ExitProgram((e.GetType() + " occurred while attempting to open " + port.name + ". Is this port already in use in another application?"), exitCode: -1); } else { ExitProgram((e.GetType() + " occurred while attempting to open " + port.name + "."), exitCode: -1); } } // if auto-connect is enabled, prepare to try again serialPort.Dispose(); Thread.Sleep(1000); // putting a delay here to avoid gobbling tons of resources thruogh constant high-speed re-connect attempts continue; } // if we get this far, clear the screen and send the connection message if not in 'quiet' mode Console.Clear(); if (!SimplySerial.Quiet) { Console.WriteLine(("<<< SimplySerial v{0} connected via {1} >>>\n" + "Settings : {2} baud, {3} parity, {4} data bits, {5} stop bit{6}, auto-connect {7}.\n" + "Device : {8}{9} {10}{11}{12}\n" + "---\n\nUse CTRL-X to exit.\n"), version, port.name, baud, (parity == Parity.None) ? "no" : (parity.ToString()).ToLower(), dataBits, (stopBits == StopBits.None) ? "0" : (stopBits == StopBits.One) ? "1" : (stopBits == StopBits.OnePointFive) ? "1.5" : "2", (stopBits == StopBits.One) ? "" : "s", (autoConnect == AutoConnect.ONE) ? "on" : (autoConnect == AutoConnect.ANY) ? "any" : "off", port.board.make, (port.board.make == "VID") ? ":" + port.vid : "", port.board.model, (port.board.model == "PID") ? ":" + port.pid : "", (port.board.isCircuitPython) ? " (CircuitPython-capable)" : "" ); } // this is the core functionality - loop while the serial port is open while (serialPort.IsOpen) { try { // process keypresses for transmission through the serial port if (Console.KeyAvailable) { // determine what key is pressed (including modifiers) keyInfo = Console.ReadKey(intercept: true); // exit the program if CTRL-X was pressed if ((keyInfo.Key == ConsoleKey.X) && (keyInfo.Modifiers == ConsoleModifiers.Control)) { Output("\n<<< SimplySerial session terminated via CTRL-X >>>"); ExitProgram(silent: true); } // properly process the backspace character else if (keyInfo.Key == ConsoleKey.Backspace) { serialPort.Write("\b"); Thread.Sleep(150); // sort of cheating here - by adding this delay we ensure that when we process the receive buffer it will contain the correct backspace control sequence } // everything else just gets sent right on through else { serialPort.Write(Convert.ToString(keyInfo.KeyChar)); } } // process data coming in from the serial port received = serialPort.ReadExisting(); // if anything was received, process it if (received.Length > 0) { // properly process backspace if (received == ("\b\x1B[K")) { received = "\b \b"; } // write what was received to console Console.Write(received); } } catch (Exception e) { if (autoConnect == AutoConnect.NONE) { ExitProgram((e.GetType() + " occurred while attempting to read/write to/from " + port.name + "."), exitCode: -1); } else { Output("\n<<< Communications Interrupted >>>\n"); } serialPort.Dispose(); Thread.Sleep(2000); // sort-of arbitrary delay - should be long enough to read the "interrupted" message Console.Clear(); if (autoConnect == AutoConnect.ANY) { port.name = String.Empty; Output("<<< Attemping to connect to any avaiable COM port. Use CTRL-X to cancel >>>"); } else if (autoConnect == AutoConnect.ONE) { Console.Clear(); Output("<<< Attempting to re-connect to " + port.name + ". Use CTRL-X to cancel >>>"); } break; } } } while (autoConnect > AutoConnect.NONE); // if we get to this point, we should be exiting gracefully ExitProgram("<<< SimplySerial session terminated >>>", exitCode: 0); }
static void Main(string[] args) { // attempt to enable virtual terminal escape sequence processing try { var iStdOut = GetStdHandle(STD_OUTPUT_HANDLE); GetConsoleMode(iStdOut, out uint outConsoleMode); outConsoleMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; SetConsoleMode(iStdOut, outConsoleMode); } catch { // if the above fails, it doesn't really matter - it just means escape sequences won't process nicely } // load and parse data in boards.json LoadBoards(); // process all command-line arguments ProcessArguments(args); // verify log-related settings if (logging) { try { FileStream stream = new FileStream(logFile, logMode, FileAccess.Write); using (StreamWriter writer = new StreamWriter(stream, Encoding.UTF8)) { writer.WriteLine($"\n----- LOGGING STARTED ({DateTime.Now}) ------------------------------------"); } } catch (Exception e) { logging = false; ExitProgram($"* Error accessing log file '{logFile}'\n > {e.GetType()}: {e.Message}", exitCode: -1); } } // set up keyboard input for program control / relay to serial port ConsoleKeyInfo keyInfo = new ConsoleKeyInfo(); Console.TreatControlCAsInput = true; // we need to use CTRL-C to activate the REPL in CircuitPython, so it can't be used to exit the application // this is where data read from the serial port will be temporarily stored string received = string.Empty; //main loop - keep this up until user presses CTRL-X or an exception takes us down do { // first things first, check for (and respect) a request to exit the program via CTRL-X if (Console.KeyAvailable) { keyInfo = Console.ReadKey(intercept: true); if ((keyInfo.Key == ConsoleKey.X) && (keyInfo.Modifiers == ConsoleModifiers.Control)) { Output("\n<<< SimplySerial session terminated via CTRL-X >>>"); ExitProgram(silent: true); } } // get a list of available ports availablePorts = (SimplySerial.GetSerialPorts()).OrderBy(p => p.num).ToList(); // if no port was specified/selected, pick one automatically if (port.name == String.Empty) { // if there are com ports available, pick one if (availablePorts.Count() >= 1) { // first, try to default to something that we assume is running CircuitPython SimplySerial.port = availablePorts.Find(p => p.isCircuitPython == true); // if that doesn't work out, just default to the first available COM port if (SimplySerial.port == null) { SimplySerial.port = availablePorts[0]; } } // if there are no com ports available, exit or try again depending on autoconnect setting else { if (autoConnect == AutoConnect.NONE) { ExitProgram("No COM ports detected.", exitCode: -1); } else { continue; } } } // if a specific port has been selected, try to match it with one that actually exists else { bool portMatched = false; foreach (ComPort p in availablePorts) { if (p.name == port.name) { portMatched = true; port = p; break; } } // if the specified port is not available, exit or try again depending on autoconnect setting if (!portMatched) { if (autoConnect == AutoConnect.NONE) { ExitProgram(("Invalid port specified <" + port.name + ">"), exitCode: -1); } else { continue; } } } // if we get this far, it should be safe to set up the specified/selected serial port serialPort = new SerialPort(port.name) { Handshake = Handshake.None, // we don't need to support any handshaking at this point ReadTimeout = 1, // minimal timeout - we don't want to wait forever for data that may not be coming! WriteTimeout = 250, // small delay - if we go too small on this it causes System.IO semaphore timeout exceptions DtrEnable = true, // without this we don't ever receive any data RtsEnable = true // without this we don't ever receive any data }; // attempt to set the baud rate, fail if the specified value is not supported by the hardware try { if (baud < 0) { if (port.isCircuitPython) { baud = 115200; } else { baud = 9600; } } serialPort.BaudRate = baud; } catch (ArgumentOutOfRangeException) { ExitProgram(("The specified baud rate (" + baud + ") is not supported."), exitCode: -2); } // set other port parameters (which have already been validated) serialPort.Parity = parity; serialPort.DataBits = dataBits; serialPort.StopBits = stopBits; // attempt to open the serial port, deal with failures try { serialPort.Open(); } catch (Exception e) { // if auto-connect is disabled than any exception should result in program termination if (autoConnect == AutoConnect.NONE) { if (e is UnauthorizedAccessException) { ExitProgram((e.GetType() + " occurred while attempting to open " + port.name + ". Is this port already in use in another application?"), exitCode: -1); } else { ExitProgram((e.GetType() + " occurred while attempting to open " + port.name + "."), exitCode: -1); } } // if auto-connect is enabled, prepare to try again serialPort.Dispose(); Thread.Sleep(1000); // putting a delay here to avoid gobbling tons of resources thruogh constant high-speed re-connect attempts continue; } // if we get this far, clear the screen and send the connection message if not in 'quiet' mode Console.Clear(); Output(String.Format("<<< SimplySerial v{0} connected via {1} >>>\n" + "Settings : {2} baud, {3} parity, {4} data bits, {5} stop bit{6}, auto-connect {7}.\n" + "Device : {8} {9}{10}\n{11}" + "---\n\nUse CTRL-X to exit.\n", version, port.name, baud, (parity == Parity.None) ? "no" : (parity.ToString()).ToLower(), dataBits, (stopBits == StopBits.None) ? "0" : (stopBits == StopBits.One) ? "1" : (stopBits == StopBits.OnePointFive) ? "1.5" : "2", (stopBits == StopBits.One) ? "" : "s", (autoConnect == AutoConnect.ONE) ? "on" : (autoConnect == AutoConnect.ANY) ? "any" : "off", port.board.make, port.board.model, (port.isCircuitPython) ? " (CircuitPython-capable)" : "", (logging == true) ? ($"Logfile : {logFile} (Mode = " + ((logMode == FileMode.Create) ? "OVERWRITE" : "APPEND") + ")\n") : "" ), flush: true); lastFlush = DateTime.Now; DateTime start = DateTime.Now; TimeSpan timeSinceRX = new TimeSpan(); TimeSpan timeSinceFlush = new TimeSpan(); // this is the core functionality - loop while the serial port is open while (serialPort.IsOpen) { try { // process keypresses for transmission through the serial port if (Console.KeyAvailable) { // determine what key is pressed (including modifiers) keyInfo = Console.ReadKey(intercept: true); // exit the program if CTRL-X was pressed if ((keyInfo.Key == ConsoleKey.X) && (keyInfo.Modifiers == ConsoleModifiers.Control)) { Output("\n<<< SimplySerial session terminated via CTRL-X >>>"); ExitProgram(silent: true); } // check for keys that require special processing (cursor keys, etc.) else if (specialKeys.ContainsKey(keyInfo.Key)) { serialPort.Write(specialKeys[keyInfo.Key]); } // everything else just gets sent right on through else { serialPort.Write(Convert.ToString(keyInfo.KeyChar)); } } // process data coming in from the serial port received = serialPort.ReadExisting(); // if anything was received, process it if (received.Length > 0) { if (forceNewline) { received = received.Replace("\r", "\n"); } // write what was received to console Output(received, force: true, newline: false); start = DateTime.Now; } else { Thread.Sleep(1); } if (logging) { timeSinceRX = DateTime.Now - start; timeSinceFlush = DateTime.Now - lastFlush; if ((timeSinceRX.TotalSeconds >= 2) || (timeSinceFlush.TotalSeconds >= 10)) { if (logData.Length > 0) { Output("", force: true, newline: false, flush: true); } start = DateTime.Now; lastFlush = DateTime.Now; } } // if the serial port is unexpectedly closed, throw an exception if (!serialPort.IsOpen) { throw new IOException(); } } catch (Exception e) { if (autoConnect == AutoConnect.NONE) { ExitProgram((e.GetType() + " occurred while attempting to read/write to/from " + port.name + "."), exitCode: -1); } else { Output("\n<<< Communications Interrupted >>>\n"); } try { serialPort.Dispose(); } catch { //nothing to do here, other than prevent execution from stopping if dispose() throws an exception } Thread.Sleep(2000); // sort-of arbitrary delay - should be long enough to read the "interrupted" message if (autoConnect == AutoConnect.ANY) { port.name = String.Empty; Output("<<< Attemping to connect to any available COM port. Use CTRL-X to cancel >>>"); } else if (autoConnect == AutoConnect.ONE) { Output("<<< Attempting to re-connect to " + port.name + ". Use CTRL-X to cancel >>>"); } break; } } } while (autoConnect > AutoConnect.NONE); // if we get to this point, we should be exiting gracefully ExitProgram("<<< SimplySerial session terminated >>>", exitCode: 0); }