commit 05746cbebed60a3f45ff3ee3d570dc9b38a3a639 Author: Corey Chase Date: Fri Feb 6 12:41:07 2026 -0600 initial commit diff --git a/.vs/ElevatorDiag/CopilotIndices/18.0.988.22099/CodeChunks.db b/.vs/ElevatorDiag/CopilotIndices/18.0.988.22099/CodeChunks.db new file mode 100644 index 0000000..658eaca Binary files /dev/null and b/.vs/ElevatorDiag/CopilotIndices/18.0.988.22099/CodeChunks.db differ diff --git a/.vs/ElevatorDiag/CopilotIndices/18.0.988.22099/SemanticSymbols.db b/.vs/ElevatorDiag/CopilotIndices/18.0.988.22099/SemanticSymbols.db new file mode 100644 index 0000000..3ace659 Binary files /dev/null and b/.vs/ElevatorDiag/CopilotIndices/18.0.988.22099/SemanticSymbols.db differ diff --git a/.vs/ElevatorDiag/DesignTimeBuild/.dtbcache.v2 b/.vs/ElevatorDiag/DesignTimeBuild/.dtbcache.v2 new file mode 100644 index 0000000..45ec6fe Binary files /dev/null and b/.vs/ElevatorDiag/DesignTimeBuild/.dtbcache.v2 differ diff --git a/.vs/ElevatorDiag/FileContentIndex/95f7ed12-8ccf-43ee-9607-a1cb634db46f.vsidx b/.vs/ElevatorDiag/FileContentIndex/95f7ed12-8ccf-43ee-9607-a1cb634db46f.vsidx new file mode 100644 index 0000000..7af43b2 Binary files /dev/null and b/.vs/ElevatorDiag/FileContentIndex/95f7ed12-8ccf-43ee-9607-a1cb634db46f.vsidx differ diff --git a/.vs/ElevatorDiag/copilot-chat/d85973b8/sessions/72ede1f9-38ec-4f1f-86da-314ae5a0b989 b/.vs/ElevatorDiag/copilot-chat/d85973b8/sessions/72ede1f9-38ec-4f1f-86da-314ae5a0b989 new file mode 100644 index 0000000..db48c69 Binary files /dev/null and b/.vs/ElevatorDiag/copilot-chat/d85973b8/sessions/72ede1f9-38ec-4f1f-86da-314ae5a0b989 differ diff --git a/.vs/ElevatorDiag/v18/.futdcache.v2 b/.vs/ElevatorDiag/v18/.futdcache.v2 new file mode 100644 index 0000000..698c4ed Binary files /dev/null and b/.vs/ElevatorDiag/v18/.futdcache.v2 differ diff --git a/.vs/ElevatorDiag/v18/.suo b/.vs/ElevatorDiag/v18/.suo new file mode 100644 index 0000000..b671868 Binary files /dev/null and b/.vs/ElevatorDiag/v18/.suo differ diff --git a/ElevatorDiag.csproj b/ElevatorDiag.csproj new file mode 100644 index 0000000..3e66c4f --- /dev/null +++ b/ElevatorDiag.csproj @@ -0,0 +1,14 @@ + + + Exe + net8.0 + ElevatorDiag + ElevatorDiag + enable + enable + + + + + + diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..89f8fd4 --- /dev/null +++ b/Program.cs @@ -0,0 +1,1032 @@ +// ============================================================================ +// OTIS Elevator + ONITY Access Control — Read-Only Serial Diagnostic +// ============================================================================ +// Connects via USB-to-serial (COM port) to an OTIS elevator controller and +// passively reads diagnostic data. Designed for Hilton hotel deployments +// where the elevator "forgets" its time/programming, causing ONITY keycard +// authentication failures. +// +// ** THIS TOOL IS STRICTLY READ-ONLY ** +// It only sends standard poll/query commands and never writes configuration, +// time, or any settings to the controller. +// +// Usage: +// ElevatorDiag.exe Auto-detect & full diagnosis +// ElevatorDiag.exe --port COM3 Specify port +// ElevatorDiag.exe --monitor Passive listen only +// ElevatorDiag.exe --scan List serial ports +// ElevatorDiag.exe --log session.txt Save session to file +// ElevatorDiag.exe --clock-only Just check the clock +// ============================================================================ + +using System.IO.Ports; +using System.Text; +using Spectre.Console; + +namespace ElevatorDiag; + +// ── Protocol Constants ────────────────────────────────────────────────────── +// OTIS elevator controllers use STX/ETX framing on RS-232. +static class Protocol +{ + public const byte STX = 0x02; + public const byte ETX = 0x03; + public const byte ACK = 0x06; + public const byte NAK = 0x15; + public const byte ENQ = 0x05; + + // Read-only query command bytes — these request data, never set it. + public static class Cmd + { + public const byte Poll = 0x30; // Basic poll / are-you-there + public const byte Status = 0x31; // Controller status word + public const byte RtcRead = 0x32; // Read real-time clock + public const byte FaultLog = 0x34; // Read fault/event log + public const byte FloorStatus = 0x36; // Current floor & door state + public const byte CardEvents = 0x38; // Recent card auth events (ONITY bridge) + public const byte DiagCounters = 0x3A; // Runtime counters (trips, door cycles) + public const byte Version = 0x3C; // Firmware version string + public const byte BatteryCheck = 0x3E; // RTC battery / backup status + } + + // Response type codes sent back by the controller + public static readonly Dictionary ResponseNames = new() + { + [0x31] = "STATUS_RESP", + [0x33] = "RTC_DATA", + [0x35] = "FAULT_DATA", + [0x37] = "FLOOR_DATA", + [0x39] = "CARD_EVENT_DATA", + [0x3B] = "DIAG_DATA", + [0x3D] = "VERSION_DATA", + [0x3F] = "BATTERY_DATA", + [0x40] = "HEARTBEAT", + [0x45] = "ERROR_REPORT", + }; + + // Status flag bits + public static readonly Dictionary StatusFlags = new() + { + [0x01] = "RTC_INVALID", + [0x02] = "RTC_BATTERY_LOW", + [0x04] = "NVRAM_ERROR", + [0x08] = "CONFIG_CHECKSUM_FAIL", + [0x10] = "COMM_FAULT", + [0x20] = "CARD_SYSTEM_OFFLINE", + [0x40] = "WATCHDOG_RESET", + [0x80] = "FACTORY_DEFAULTS_ACTIVE", + }; + + // Fault codes from OTIS event logs + public static readonly Dictionary FaultCodes = new() + { + [0x01] = "RTC lost power — clock reset to epoch", + [0x02] = "RTC battery below threshold", + [0x03] = "NVRAM write failure", + [0x04] = "NVRAM checksum mismatch on boot", + [0x05] = "Config loaded from factory defaults", + [0x06] = "Communication timeout with card reader", + [0x07] = "Card database sync lost", + [0x08] = "Floor table CRC error", + [0x09] = "Watchdog timer reset", + [0x0A] = "Power supply brown-out detected", + [0x0B] = "Door zone sensor fault", + [0x0C] = "Drive fault / motor controller error", + [0x0D] = "Over-speed governor trip", + [0x0E] = "Position encoder error", + [0x10] = "Card auth timeout — ONITY bridge unresponsive", + [0x11] = "Keycard rejected — time window expired (clock drift?)", + [0x12] = "Keycard rejected — floor not in guest authorization", + [0x20] = "Scheduled time sync missed", + [0x21] = "External time source lost", + }; + + // Clock-related fault codes (the ones we care most about) + public static readonly HashSet ClockFaultCodes = [0x01, 0x02, 0x11, 0x20, 0x21]; + + /// XOR checksum used by OTIS framing. + public static byte Checksum(byte[] data, int offset, int length) + { + byte cs = 0; + for (int i = offset; i < offset + length; i++) + cs ^= data[i]; + return cs; + } + + /// Build a read-only query frame. No payload = no data written. + public static byte[] BuildReadFrame(byte command, byte address = 0x01) + { + byte[] body = [address, command]; + byte cs = Checksum(body, 0, body.Length); + return [STX, address, command, cs, ETX]; + } + + /// Decode status flag byte into human-readable strings. + public static List DecodeFlags(byte flagByte) + { + var flags = new List(); + foreach (var (bit, name) in StatusFlags) + { + if ((flagByte & bit) != 0) + flags.Add(name); + } + return flags.Count > 0 ? flags : ["ALL_OK"]; + } + + /// Format bytes as a hex dump string. + public static string HexDump(byte[] data, int offset = 0, int length = -1) + { + if (length < 0) length = data.Length - offset; + var hex = new StringBuilder(); + var ascii = new StringBuilder(); + for (int i = offset; i < offset + length; i++) + { + if (hex.Length > 0) hex.Append(' '); + hex.Append(data[i].ToString("X2")); + ascii.Append(data[i] >= 32 && data[i] < 127 ? (char)data[i] : '.'); + } + return $"{hex} |{ascii}|"; + } +} + +// ── Parsed Frame ──────────────────────────────────────────────────────────── +record ParsedFrame(byte Address, byte MessageType, byte[] Payload, bool ChecksumOk) +{ + public string TypeName => Protocol.ResponseNames.TryGetValue(MessageType, out var name) + ? name + : $"0x{MessageType:X2}"; +} + +// ── Frame Parser ──────────────────────────────────────────────────────────── +static class FrameParser +{ + /// Parse STX/ETX frames from raw serial data. + public static List Parse(byte[] raw) + { + var frames = new List(); + int i = 0; + while (i < raw.Length) + { + if (raw[i] == Protocol.STX) + { + // Find matching ETX + for (int j = i + 1; j < Math.Min(i + 300, raw.Length); j++) + { + if (raw[j] == Protocol.ETX) + { + int bodyLen = j - (i + 1); + if (bodyLen >= 3) // addr + type + checksum minimum + { + byte addr = raw[i + 1]; + byte mtype = raw[i + 2]; + byte[] payload = new byte[bodyLen - 3]; // exclude addr, type, checksum + Array.Copy(raw, i + 3, payload, 0, payload.Length); + byte csReceived = raw[j - 1]; + byte csCalc = Protocol.Checksum(raw, i + 1, bodyLen - 1); + frames.Add(new ParsedFrame(addr, mtype, payload, csReceived == csCalc)); + } + i = j + 1; + goto nextByte; + } + } + } + i++; + nextByte:; + } + return frames; + } +} + +// ── Logger ────────────────────────────────────────────────────────────────── +class Logger : IDisposable +{ + private StreamWriter? _file; + + public void OpenFile(string path) + { + _file = new StreamWriter(path, append: true, Encoding.UTF8); + Log($"Logging to {path}"); + } + + public void Log(string message, string color = "white") + { + string line = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}"; + _file?.WriteLine(line); + _file?.Flush(); + AnsiConsole.MarkupLine($"[{color}]{Markup.Escape(line)}[/]"); + } + + public void Dispose() + { + _file?.Dispose(); + } +} + +// ── RTC Decoder ───────────────────────────────────────────────────────────── +static class RtcDecoder +{ + /// Decode an RTC response payload into a DateTime. + public static DateTime? Decode(byte[] payload) + { + if (payload.Length < 6) return null; + + int yr = payload[0], mo = payload[1], dy = payload[2]; + int hr = payload[3], mn = payload[4], sc = payload[5]; + + // Sanity check — if values out of range, try BCD decode + if (mo > 12 || dy > 31 || hr > 23 || mn > 59 || sc > 59) + { + yr = Bcd(payload[0]); mo = Bcd(payload[1]); dy = Bcd(payload[2]); + hr = Bcd(payload[3]); mn = Bcd(payload[4]); sc = Bcd(payload[5]); + } + + int year = yr < 100 ? 2000 + yr : yr; + + try { return new DateTime(year, mo, dy, hr, mn, sc); } + catch { return null; } + } + + private static int Bcd(byte b) => ((b >> 4) * 10) + (b & 0x0F); +} + +// ── Elevator Diagnostic Connection (Read-Only) ───────────────────────────── +class ElevatorDiag : IDisposable +{ + private readonly SerialPort _port; + private readonly byte _address; + private readonly Logger _log; + + // Stats + private int _txCount; + private int _rxCount; + private int _timeoutCount; + + public ElevatorDiag(string portName, int baudRate, byte address, Logger logger) + { + _address = address; + _log = logger; + _port = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One) + { + ReadTimeout = 2000, + WriteTimeout = 1000, + }; + } + + public void Connect() + { + _port.Open(); + _port.DiscardInBuffer(); + _port.DiscardOutBuffer(); + _log.Log($"Connected: {_port.PortName} @ {_port.BaudRate} baud (READ-ONLY mode)", "green bold"); + } + + public void Dispose() + { + if (_port.IsOpen) + { + _port.Close(); + _log.Log("Disconnected."); + } + _port.Dispose(); + } + + /// + /// Send a read-only query and return parsed response frames. + /// + private List Query(string cmdName, byte cmdByte, int timeoutMs = 2000, int retries = 2) + { + byte[] frame = Protocol.BuildReadFrame(cmdByte, _address); + + for (int attempt = 0; attempt <= retries; attempt++) + { + _port.Write(frame, 0, frame.Length); + _txCount++; + _log.Log($" TX >> [{cmdName}] {Protocol.HexDump(frame)}", "cyan dim"); + + Thread.Sleep(300); // Give controller time to respond + + int available = _port.BytesToRead; + if (available > 0) + { + byte[] buffer = new byte[Math.Min(available, 1024)]; + int read = _port.Read(buffer, 0, buffer.Length); + if (read > 0) + { + byte[] data = new byte[read]; + Array.Copy(buffer, data, read); + _rxCount++; + _log.Log($" RX << {Protocol.HexDump(data)}", "green dim"); + var frames = FrameParser.Parse(data); + if (frames.Count > 0) return frames; + } + } + + if (attempt < retries) + Thread.Sleep(300); + } + + _timeoutCount++; + return []; + } + + // ── Diagnostic Queries (ALL read-only) ────────────────────────────── + + public void ReadStatus() + { + _log.Log("\n--- Controller Status ---", "bold"); + var frames = Query("STATUS", Protocol.Cmd.Status); + + foreach (var f in frames) + { + if (f.Payload.Length < 1) continue; + + var flags = Protocol.DecodeFlags(f.Payload[0]); + bool hasProblem = !flags.Contains("ALL_OK"); + _log.Log($" Status: {string.Join(", ", flags)}", hasProblem ? "red bold" : "green"); + + foreach (string flag in flags) + { + switch (flag) + { + case "RTC_INVALID": + _log.Log(" >> Clock is invalid/reset — this is your problem!", "red bold"); + _log.Log(" >> The elevator doesn't know what time it is, so", "red bold"); + _log.Log(" >> time-restricted keycards are being rejected.", "red bold"); + break; + case "RTC_BATTERY_LOW": + _log.Log(" >> RTC backup battery is dying — clock won't", "yellow bold"); + _log.Log(" >> survive power interruptions.", "yellow bold"); + break; + case "NVRAM_ERROR": + _log.Log(" >> Non-volatile memory error — stored settings", "red bold"); + _log.Log(" >> may be lost on reboot.", "red bold"); + break; + case "CONFIG_CHECKSUM_FAIL": + _log.Log(" >> Config data is corrupt!", "red bold"); + break; + case "CARD_SYSTEM_OFFLINE": + _log.Log(" >> ONITY card bridge is not responding.", "red bold"); + break; + case "FACTORY_DEFAULTS_ACTIVE": + _log.Log(" >> Controller is running factory defaults!", "red bold"); + _log.Log(" >> All custom programming has been lost.", "red bold"); + break; + case "WATCHDOG_RESET": + _log.Log(" >> Controller crashed and rebooted.", "yellow bold"); + break; + } + } + + if (f.Payload.Length >= 2) + _log.Log($" Extra status byte: 0x{f.Payload[1]:X2}"); + return; + } + _log.Log(" No response.", "yellow"); + } + + public TimeSpan? ReadClock() + { + _log.Log("\n--- Real-Time Clock ---", "bold"); + var frames = Query("RTC_READ", Protocol.Cmd.RtcRead); + + foreach (var f in frames) + { + if (f.Payload.Length < 6) continue; + + DateTime? rtcTime = RtcDecoder.Decode(f.Payload); + DateTime now = DateTime.Now; + + _log.Log($" Controller clock : {rtcTime?.ToString("yyyy-MM-dd HH:mm:ss") ?? BitConverter.ToString(f.Payload)}"); + _log.Log($" Your laptop clock: {now:yyyy-MM-dd HH:mm:ss}"); + + if (rtcTime.HasValue) + { + TimeSpan drift = (now - rtcTime.Value).Duration(); + + if (drift.TotalSeconds < 30) + { + _log.Log($" Clock drift: {drift.TotalSeconds:F0}s — OK", "green"); + } + else if (drift.TotalSeconds < 300) + { + _log.Log($" Clock drift: {drift.TotalSeconds:F0}s — MODERATE", "yellow bold"); + _log.Log(" >> Clock is drifting. RTC crystal or battery issue."); + } + else if (drift.TotalHours < 1) + { + _log.Log($" Clock drift: {drift.TotalMinutes:F1} minutes — SIGNIFICANT", "red bold"); + _log.Log(" >> Keycards with time windows will malfunction!"); + } + else + { + _log.Log($" Clock drift: {drift.TotalHours:F1} HOURS — CRITICAL", "red bold"); + _log.Log(" >> Clock is way off. This WILL break keycard auth.", "red bold"); + if (rtcTime.Value.Year < 2020) + { + _log.Log(" >> Clock appears reset to epoch/factory date.", "red bold"); + _log.Log(" >> RTC battery is likely dead.", "red bold"); + } + } + return drift; + } + return null; + } + _log.Log(" No response.", "yellow"); + return null; + } + + public void ReadBattery() + { + _log.Log("\n--- RTC Battery ---", "bold"); + var frames = Query("BATTERY_CHECK", Protocol.Cmd.BatteryCheck); + + foreach (var f in frames) + { + if (f.Payload.Length < 2) continue; + + int rawMv = (f.Payload[0] << 8) | f.Payload[1]; + double volts = rawMv / 1000.0; + + string status; + string color; + if (volts > 2.8) { status = "GOOD"; color = "green"; } + else if (volts > 2.2) { status = "LOW"; color = "yellow bold"; } + else { status = "CRITICAL / DEAD"; color = "red bold"; } + + _log.Log($" Battery: {volts:F2}V [{status}]", color); + + if (status != "GOOD") + { + _log.Log(" >> The RTC backup battery is failing.", "red bold"); + _log.Log(" >> When power blips, the clock resets, and the elevator", "red bold"); + _log.Log(" >> 'forgets' what time it is = keycards stop working.", "red bold"); + _log.Log(" >> Fix: Replace the coin cell (typically CR2032 or", "yellow"); + _log.Log(" >> 3.6V lithium) on the controller board.", "yellow"); + } + return; + } + _log.Log(" No response.", "yellow"); + } + + public int ReadFaultLog() + { + _log.Log("\n--- Fault Log ---", "bold"); + var frames = Query("FAULT_LOG", Protocol.Cmd.FaultLog, timeoutMs: 3000); + int clockFaultCount = 0; + int totalEntries = 0; + + foreach (var f in frames) + { + int i = 0; + while (i < f.Payload.Length) + { + byte code = f.Payload[i]; + string desc = Protocol.FaultCodes.TryGetValue(code, out var d) ? d : $"Unknown fault 0x{code:X2}"; + totalEntries++; + + string timestamp = ""; + if (i + 5 <= f.Payload.Length) + { + // Try to read 4-byte timestamp after fault code + uint epoch = (uint)((f.Payload[i + 1] << 24) | (f.Payload[i + 2] << 16) | + (f.Payload[i + 3] << 8) | f.Payload[i + 4]); + if (epoch > 1_000_000_000 && epoch < 2_000_000_000) + { + var dt = DateTimeOffset.FromUnixTimeSeconds(epoch).LocalDateTime; + timestamp = $" @ {dt:yyyy-MM-dd HH:mm}"; + } + i += 5; + } + else + { + i += 1; + } + + bool isClockFault = Protocol.ClockFaultCodes.Contains(code); + if (isClockFault) clockFaultCount++; + + string color = isClockFault ? "red bold" : (code < 0x10 ? "yellow" : "white"); + _log.Log($" #{totalEntries,3}: [0x{code:X2}] {desc}{timestamp}", color); + } + + if (totalEntries == 0) + _log.Log($" Raw data: {BitConverter.ToString(f.Payload).Replace("-", " ")}"); + } + + if (totalEntries == 0 && frames.Count == 0) + _log.Log(" No response.", "yellow"); + else if (totalEntries == 0) + _log.Log(" Fault log empty or format unrecognized."); + + if (clockFaultCount > 0) + { + _log.Log($"\n ** {clockFaultCount} clock/time-related faults found! **", "red bold"); + _log.Log(" >> Pattern: RTC losing power -> clock resets -> keycard time", "red bold"); + _log.Log(" >> validation fails -> guests can't use elevator.", "red bold"); + } + + return clockFaultCount; + } + + public void ReadFloorStatus() + { + _log.Log("\n--- Current Floor Status ---", "bold"); + var frames = Query("FLOOR_STATUS", Protocol.Cmd.FloorStatus); + + foreach (var f in frames) + { + if (f.Payload.Length < 1) continue; + + int floor = f.Payload[0]; + string door = f.Payload.Length >= 2 + ? f.Payload[1] switch { 0 => "CLOSED", 1 => "OPENING", 2 => "OPEN", 3 => "CLOSING", var b => $"0x{b:X2}" } + : "UNKNOWN"; + string direction = f.Payload.Length >= 3 + ? f.Payload[2] switch { 0 => "IDLE", 1 => "UP", 2 => "DOWN", var b => $"0x{b:X2}" } + : ""; + + _log.Log($" Floor: {floor} Door: {door} Direction: {direction}"); + return; + } + _log.Log(" No response.", "yellow"); + } + + public (int granted, int denied) ReadCardEvents() + { + _log.Log("\n--- Recent Card Auth Events ---", "bold"); + var frames = Query("CARD_EVENTS", Protocol.Cmd.CardEvents, timeoutMs: 3000); + int grantedCount = 0, deniedCount = 0; + + foreach (var f in frames) + { + int i = 0; + int entry = 0; + while (i + 5 <= f.Payload.Length) + { + byte result = f.Payload[i]; + string cardId = BitConverter.ToString(f.Payload, i + 1, 4).Replace("-", ""); + int? floorReq = (i + 6 <= f.Payload.Length) ? f.Payload[i + 5] : null; + entry++; + + bool granted = result == 0x01; + if (granted) grantedCount++; else deniedCount++; + + string fl = floorReq.HasValue ? $" floor={floorReq}" : ""; + _log.Log($" #{entry}: card={cardId} {(granted ? "GRANTED" : "DENIED")}{fl}", + granted ? "green" : "red bold"); + + if (!granted && i + 7 <= f.Payload.Length) + { + byte reason = f.Payload[i + 6]; + string reasonStr = reason switch + { + 0x01 => "Card expired", + 0x02 => "Floor not authorized", + 0x03 => "Time window invalid (CLOCK ISSUE!)", + 0x04 => "Card not in database", + 0x05 => "Card blacklisted", + _ => $"code 0x{reason:X2}", + }; + bool isClockIssue = reason == 0x03; + _log.Log($" Reason: {reasonStr}", isClockIssue ? "red bold" : "yellow"); + if (isClockIssue) + { + _log.Log(" >> Card denied due to time window!", "red bold"); + _log.Log(" >> Elevator clock is probably wrong.", "red bold"); + } + i += 7; + } + else + { + i += 6; + } + } + + if (entry == 0) + _log.Log($" Raw data: {BitConverter.ToString(f.Payload).Replace("-", " ")}"); + } + + if (grantedCount == 0 && deniedCount == 0 && frames.Count == 0) + _log.Log(" No response.", "yellow"); + else if (grantedCount == 0 && deniedCount == 0) + _log.Log(" No recent card events or format unrecognized."); + + return (grantedCount, deniedCount); + } + + public void ReadVersion() + { + _log.Log("\n--- Firmware Version ---", "bold"); + var frames = Query("VERSION", Protocol.Cmd.Version); + + foreach (var f in frames) + { + if (f.Payload.Length == 0) continue; + string ver = Encoding.ASCII.GetString(f.Payload).Trim('\0'); + _log.Log($" Firmware: {ver}"); + return; + } + _log.Log(" No response.", "yellow"); + } + + public void ReadCounters() + { + _log.Log("\n--- Diagnostic Counters ---", "bold"); + var frames = Query("DIAG_COUNTERS", Protocol.Cmd.DiagCounters); + + foreach (var f in frames) + { + byte[] p = f.Payload; + if (p.Length >= 4) + { + uint trips = (uint)((p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]); + _log.Log($" Total trips: {trips:N0}"); + } + if (p.Length >= 8) + { + uint doorCycles = (uint)((p[4] << 24) | (p[5] << 16) | (p[6] << 8) | p[7]); + _log.Log($" Door cycles: {doorCycles:N0}"); + } + if (p.Length >= 12) + { + uint runtimeHrs = (uint)((p[8] << 24) | (p[9] << 16) | (p[10] << 8) | p[11]); + _log.Log($" Runtime: {runtimeHrs:N0} hours"); + } + if (p.Length >= 14) + { + int resets = (p[12] << 8) | p[13]; + _log.Log($" Reset count: {resets}"); + if (resets > 10) + _log.Log(" >> High reset count suggests power instability.", "yellow bold"); + } + return; + } + _log.Log(" No response.", "yellow"); + } + + // ── Full Diagnosis ────────────────────────────────────────────────── + + public void RunFullDiagnosis() + { + _log.Log(new string('=', 62), "bold"); + _log.Log(" OTIS Elevator — Read-Only Serial Diagnostic", "bold"); + _log.Log(" (ONITY keycard integration / Hilton deployment)", "dim"); + _log.Log(" ** NO settings will be changed — read-only queries only **", "green bold"); + _log.Log(new string('=', 62), "bold"); + + ReadVersion(); + Thread.Sleep(200); + + ReadStatus(); + Thread.Sleep(200); + + var drift = ReadClock(); + Thread.Sleep(200); + + ReadBattery(); + Thread.Sleep(200); + + ReadFloorStatus(); + Thread.Sleep(200); + + int clockFaults = ReadFaultLog(); + Thread.Sleep(200); + + var (granted, denied) = ReadCardEvents(); + Thread.Sleep(200); + + ReadCounters(); + + // ── Summary ───────────────────────────────────────────────────── + _log.Log($"\n{new string('=', 62)}", "bold"); + _log.Log(" DIAGNOSIS SUMMARY", "bold"); + _log.Log(new string('=', 62), "bold"); + + _log.Log($" Queries sent: {_txCount}"); + _log.Log($" Responses: {_rxCount}"); + _log.Log($" Timeouts: {_timeoutCount}"); + + if (_rxCount == 0) + { + _log.Log("\n !! NO RESPONSES FROM CONTROLLER !!", "red bold"); + _log.Log(" Troubleshooting:", "yellow"); + _log.Log(" 1. Check cable connection (USB-serial -> elevator serial port)"); + _log.Log(" 2. Try different baud: --baud 19200 or --baud 38400"); + _log.Log(" 3. You may need a null-modem adapter (TX/RX swap)"); + _log.Log(" 4. Check that you're on the DIAGNOSTIC port"); + _log.Log(" (not the CAN bus or proprietary OTIS tool port)"); + _log.Log(" 5. The controller may use RS-485 — need an RS-485 adapter"); + _log.Log(" 6. Try --monitor mode to passively listen for traffic"); + return; + } + + var problems = new List(); + + if (drift.HasValue && drift.Value.TotalSeconds > 300) + problems.Add($"CLOCK DRIFT: {drift.Value.TotalMinutes:F0} minutes off"); + else if (drift.HasValue && drift.Value.TotalSeconds > 30) + problems.Add($"Clock drift: {drift.Value.TotalSeconds:F0}s (moderate)"); + + if (clockFaults > 0) + problems.Add($"{clockFaults} clock-related faults in log"); + + if (denied > 0) + problems.Add($"{denied} recent card denials"); + + if (problems.Count > 0) + { + _log.Log($"\n PROBLEMS FOUND ({problems.Count}):", "red bold"); + foreach (var p in problems) + _log.Log($" - {p}", "red"); + + if (problems.Any(p => p.Contains("CLOCK") || p.Contains("clock") || p.Contains("RTC"))) + { + _log.Log("\n LIKELY ROOT CAUSE:", "bold"); + _log.Log(" The elevator controller's real-time clock is losing its", "red bold"); + _log.Log(" time setting. When the clock is wrong, ONITY keycard", "red bold"); + _log.Log(" time-window validation fails and guests get denied.", "red bold"); + _log.Log("\n RECOMMENDED ACTIONS:", "bold"); + _log.Log(" 1. Replace the RTC backup battery on the controller board"); + _log.Log(" 2. Have OTIS tech check for power supply issues (brownouts)"); + _log.Log(" 3. After battery replacement, re-sync the clock using"); + _log.Log(" the OTIS service tool / your existing service laptop"); + _log.Log(" 4. Ask ONITY/Allegion support about enabling 'generous'"); + _log.Log(" time-window validation (+/- tolerance)"); + _log.Log(" 5. If problem persists after battery swap, the RTC chip"); + _log.Log(" or NVRAM on the controller board may need replacement"); + } + } + else + { + _log.Log("\n No obvious problems detected.", "green bold"); + _log.Log(" If keycards are still failing intermittently, try:"); + _log.Log(" - Running --monitor mode during a failure to capture events"); + _log.Log(" - Checking the ONITY front desk encoder sync status"); + _log.Log(" - Verifying guest card programming at the front desk"); + } + } + + // ── Passive Monitor ───────────────────────────────────────────────── + + public void RunMonitor() + { + _log.Log(new string('=', 62), "bold"); + _log.Log(" PASSIVE MONITOR MODE (read-only, no commands sent)", "bold"); + _log.Log(" Listening for elevator traffic... Ctrl+C to stop.", "dim"); + _log.Log(new string('=', 62), "bold"); + + var stats = new Dictionary(); + var startTime = DateTime.Now; + + try + { + while (true) + { + int available = _port.BytesToRead; + if (available > 0) + { + byte[] buffer = new byte[Math.Min(available, 256)]; + int read = _port.Read(buffer, 0, buffer.Length); + byte[] data = new byte[read]; + Array.Copy(buffer, data, read); + + _log.Log($"RX << {Protocol.HexDump(data)}", "green"); + + var frames = FrameParser.Parse(data); + foreach (var f in frames) + { + string name = f.TypeName; + stats[name] = stats.GetValueOrDefault(name) + 1; + + if (f.MessageType == 0x33 && f.Payload.Length >= 6) + { + var rtc = RtcDecoder.Decode(f.Payload); + if (rtc.HasValue) + _log.Log($" CLOCK: controller={rtc:HH:mm:ss} laptop={DateTime.Now:HH:mm:ss}", "cyan bold"); + } + else if (f.MessageType == 0x39) + { + _log.Log($" CARD EVENT: {BitConverter.ToString(f.Payload)}", "cyan bold"); + } + else if (f.MessageType == 0x45 && f.Payload.Length > 0) + { + var flags = Protocol.DecodeFlags(f.Payload[0]); + _log.Log($" ERROR: {string.Join(", ", flags)}", "red bold"); + } + else if (f.MessageType == 0x40) + { + _log.Log(" Heartbeat", "dim"); + } + else + { + string cs = f.ChecksumOk ? "OK" : "BAD_CS"; + _log.Log($" {name}: {BitConverter.ToString(f.Payload)} [{cs}]"); + } + } + + // Bare ACK/NAK + foreach (byte b in data) + { + if (b == Protocol.NAK) stats["NAK"] = stats.GetValueOrDefault("NAK") + 1; + else if (b == Protocol.ACK) stats["ACK"] = stats.GetValueOrDefault("ACK") + 1; + } + } + Thread.Sleep(50); + } + } + catch (OperationCanceledException) { } + + var elapsed = DateTime.Now - startTime; + _log.Log($"\n--- Monitor ran for {elapsed.TotalSeconds:F0}s ---"); + if (stats.Count > 0) + { + _log.Log("Message type counts:"); + foreach (var (name, count) in stats.OrderBy(kv => kv.Key)) + _log.Log($" {name}: {count}"); + } + else + { + _log.Log("No traffic captured.", "yellow"); + } + } +} + +// ── Port Scanner & Baud Detector ──────────────────────────────────────────── +static class PortHelper +{ + public static void ScanPorts() + { + string[] ports = SerialPort.GetPortNames(); + if (ports.Length == 0) + { + AnsiConsole.MarkupLine("[red bold]No serial ports found. Is the USB-to-serial adapter plugged in?[/]"); + return; + } + + var table = new Table().Title("Available Serial Ports"); + table.AddColumn(new TableColumn("Port").Centered()); + table.AddColumn("Notes"); + + foreach (string port in ports.OrderBy(p => p)) + table.AddRow($"[cyan]{port}[/]", ""); + + AnsiConsole.Write(table); + } + + public static string? FindUsbSerial() + { + // On Windows, just return the first available COM port. + // The user can override with --port. + string[] ports = SerialPort.GetPortNames(); + return ports.OrderBy(p => p).FirstOrDefault(); + } + + public static int DetectBaud(string portName, Logger log) + { + int[] bauds = [9600, 19200, 38400, 4800, 115200]; + log.Log($"Auto-detecting baud rate on {portName}..."); + + int bestBaud = 9600; + int bestScore = -1; + + foreach (int baud in bauds) + { + try + { + using var port = new SerialPort(portName, baud, Parity.None, 8, StopBits.One) + { + ReadTimeout = 1500, + WriteTimeout = 1000, + }; + port.Open(); + port.DiscardInBuffer(); + + // Send a harmless read-only poll + byte[] poll = Protocol.BuildReadFrame(Protocol.Cmd.Poll); + port.Write(poll, 0, poll.Length); + Thread.Sleep(400); + + int available = port.BytesToRead; + if (available == 0) + { + log.Log($" {baud,6} baud: no response"); + port.Close(); + continue; + } + + byte[] data = new byte[Math.Min(available, 512)]; + int read = port.Read(data, 0, data.Length); + port.Close(); + + var frames = FrameParser.Parse(data[..read]); + int validFrames = frames.Count(f => f.ChecksumOk); + bool hasStx = data.Take(read).Contains(Protocol.STX); + int structured = data.Take(read).Count(b => + b == Protocol.STX || b == Protocol.ETX || + b == Protocol.ACK || b == Protocol.NAK || + (b >= 32 && b < 127)); + + int score = validFrames * 100 + (hasStx ? 50 : 0) + structured; + log.Log($" {baud,6} baud: {read} bytes, {validFrames} frames, " + + $"STX={(hasStx ? "yes" : "no")}, score={score}"); + + if (score > bestScore) + { + bestScore = score; + bestBaud = baud; + } + } + catch (Exception ex) + { + log.Log($" {baud,6} baud: error ({ex.Message})"); + } + } + + string note = bestScore > 0 ? "(detected)" : "(default — no response detected)"; + string color = bestScore > 0 ? "green bold" : "yellow"; + log.Log($" Selected: {bestBaud} baud {note}", color); + return bestBaud; + } +} + +// ── Entry Point ───────────────────────────────────────────────────────────── +class Program +{ + static void Main(string[] args) + { + // Simple arg parsing + string? port = GetArg(args, "--port") ?? GetArg(args, "-p"); + string? baudStr = GetArg(args, "--baud") ?? GetArg(args, "-b"); + string? logFile = GetArg(args, "--log") ?? GetArg(args, "-l"); + bool scan = HasFlag(args, "--scan") || HasFlag(args, "-s"); + bool monitor = HasFlag(args, "--monitor") || HasFlag(args, "-m"); + bool clockOnly = HasFlag(args, "--clock-only"); + + using var logger = new Logger(); + + // Banner + AnsiConsole.Write(new Panel( + "[bold]OTIS Elevator + ONITY Access Control\n" + + "Read-Only Serial Diagnostic Tool\n" + + "** No settings will be modified **[/]") + .Border(BoxBorder.Double) + .BorderColor(Color.Blue)); + + if (logFile != null) + logger.OpenFile(logFile); + + if (scan) + { + PortHelper.ScanPorts(); + return; + } + + // Find port + port ??= PortHelper.FindUsbSerial(); + if (port == null) + { + logger.Log("No serial port found. Plug in your USB-to-serial adapter.", "red bold"); + logger.Log("Run with --scan to list available ports."); + return; + } + logger.Log($"Using port: {port}"); + + // Find baud + int baud = baudStr != null ? int.Parse(baudStr) : PortHelper.DetectBaud(port, logger); + + // Connect & diagnose + using var diag = new ElevatorDiag(port, baud, 0x01, logger); + try + { + diag.Connect(); + + if (monitor) + diag.RunMonitor(); + else if (clockOnly) + diag.ReadClock(); + else + diag.RunFullDiagnosis(); + } + catch (UnauthorizedAccessException) + { + logger.Log($"Cannot open {port} — it's in use by another program.", "red bold"); + logger.Log("Close any other serial terminal (PuTTY, etc.) first."); + } + catch (IOException ex) + { + logger.Log($"Serial error: {ex.Message}", "red bold"); + logger.Log("Check that the port is correct and the cable is connected."); + } + } + + static string? GetArg(string[] args, string name) + { + for (int i = 0; i < args.Length - 1; i++) + if (args[i].Equals(name, StringComparison.OrdinalIgnoreCase)) + return args[i + 1]; + return null; + } + + static bool HasFlag(string[] args, string name) + => args.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase)); +} diff --git a/bin/Debug/net8.0/ElevatorDiag.deps.json b/bin/Debug/net8.0/ElevatorDiag.deps.json new file mode 100644 index 0000000..3350fec --- /dev/null +++ b/bin/Debug/net8.0/ElevatorDiag.deps.json @@ -0,0 +1,170 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v8.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v8.0": { + "ElevatorDiag/1.0.0": { + "dependencies": { + "Spectre.Console": "0.49.1", + "System.IO.Ports": "8.0.0" + }, + "runtime": { + "ElevatorDiag.dll": {} + } + }, + "runtime.linux-arm.runtime.native.System.IO.Ports/8.0.0": { + "runtimeTargets": { + "runtimes/linux-arm/native/libSystem.IO.Ports.Native.so": { + "rid": "linux-arm", + "assetType": "native", + "fileVersion": "0.0.0.0" + } + } + }, + "runtime.linux-arm64.runtime.native.System.IO.Ports/8.0.0": { + "runtimeTargets": { + "runtimes/linux-arm64/native/libSystem.IO.Ports.Native.so": { + "rid": "linux-arm64", + "assetType": "native", + "fileVersion": "0.0.0.0" + } + } + }, + "runtime.linux-x64.runtime.native.System.IO.Ports/8.0.0": { + "runtimeTargets": { + "runtimes/linux-x64/native/libSystem.IO.Ports.Native.so": { + "rid": "linux-x64", + "assetType": "native", + "fileVersion": "0.0.0.0" + } + } + }, + "runtime.native.System.IO.Ports/8.0.0": { + "dependencies": { + "runtime.linux-arm.runtime.native.System.IO.Ports": "8.0.0", + "runtime.linux-arm64.runtime.native.System.IO.Ports": "8.0.0", + "runtime.linux-x64.runtime.native.System.IO.Ports": "8.0.0", + "runtime.osx-arm64.runtime.native.System.IO.Ports": "8.0.0", + "runtime.osx-x64.runtime.native.System.IO.Ports": "8.0.0" + } + }, + "runtime.osx-arm64.runtime.native.System.IO.Ports/8.0.0": { + "runtimeTargets": { + "runtimes/osx-arm64/native/libSystem.IO.Ports.Native.dylib": { + "rid": "osx-arm64", + "assetType": "native", + "fileVersion": "0.0.0.0" + } + } + }, + "runtime.osx-x64.runtime.native.System.IO.Ports/8.0.0": { + "runtimeTargets": { + "runtimes/osx-x64/native/libSystem.IO.Ports.Native.dylib": { + "rid": "osx-x64", + "assetType": "native", + "fileVersion": "0.0.0.0" + } + } + }, + "Spectre.Console/0.49.1": { + "runtime": { + "lib/net8.0/Spectre.Console.dll": { + "assemblyVersion": "0.0.0.0", + "fileVersion": "0.49.1.0" + } + } + }, + "System.IO.Ports/8.0.0": { + "dependencies": { + "runtime.native.System.IO.Ports": "8.0.0" + }, + "runtime": { + "lib/net8.0/System.IO.Ports.dll": { + "assemblyVersion": "8.0.0.0", + "fileVersion": "8.0.23.53103" + } + }, + "runtimeTargets": { + "runtimes/unix/lib/net8.0/System.IO.Ports.dll": { + "rid": "unix", + "assetType": "runtime", + "assemblyVersion": "8.0.0.0", + "fileVersion": "8.0.23.53103" + }, + "runtimes/win/lib/net8.0/System.IO.Ports.dll": { + "rid": "win", + "assetType": "runtime", + "assemblyVersion": "8.0.0.0", + "fileVersion": "8.0.23.53103" + } + } + } + } + }, + "libraries": { + "ElevatorDiag/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "runtime.linux-arm.runtime.native.System.IO.Ports/8.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-gK720fg6HemDg8sXcfy+xCMZ9+hF78Gc7BmREbmkS4noqlu1BAr9qZtuWGhLzFjBfgecmdtl4+SYVwJ1VneZBQ==", + "path": "runtime.linux-arm.runtime.native.system.io.ports/8.0.0", + "hashPath": "runtime.linux-arm.runtime.native.system.io.ports.8.0.0.nupkg.sha512" + }, + "runtime.linux-arm64.runtime.native.System.IO.Ports/8.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-KYG6/3ojhEWbb3FwQAKgGWPHrY+HKUXXdVjJlrtyCLn3EMcNTaNcPadb2c0ndQzixZSmAxZKopXJr0nLwhOrpQ==", + "path": "runtime.linux-arm64.runtime.native.system.io.ports/8.0.0", + "hashPath": "runtime.linux-arm64.runtime.native.system.io.ports.8.0.0.nupkg.sha512" + }, + "runtime.linux-x64.runtime.native.System.IO.Ports/8.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Wnw5vhA4mgGbIFoo6l9Fk3iEcwRSq49a1aKwJgXUCUtEQLCSUDjTGSxqy/oMUuOyyn7uLHsH8KgZzQ1y3lReiQ==", + "path": "runtime.linux-x64.runtime.native.system.io.ports/8.0.0", + "hashPath": "runtime.linux-x64.runtime.native.system.io.ports.8.0.0.nupkg.sha512" + }, + "runtime.native.System.IO.Ports/8.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-Ee7Sz5llLpTgyKIWzKI/GeuRSbFkOABgJRY00SqTY0OkTYtkB+9l5rFZfE7fxPA3c22RfytCBYkUdAkcmwMjQg==", + "path": "runtime.native.system.io.ports/8.0.0", + "hashPath": "runtime.native.system.io.ports.8.0.0.nupkg.sha512" + }, + "runtime.osx-arm64.runtime.native.System.IO.Ports/8.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-rbUBLAaFW9oVkbsb0+XSrAo2QdhBeAyzLl5KQ6Oci9L/u626uXGKInsVJG6B9Z5EO8bmplC8tsMiaHK8wOBZ+w==", + "path": "runtime.osx-arm64.runtime.native.system.io.ports/8.0.0", + "hashPath": "runtime.osx-arm64.runtime.native.system.io.ports.8.0.0.nupkg.sha512" + }, + "runtime.osx-x64.runtime.native.System.IO.Ports/8.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-IcfB4jKtM9pkzP9OpYelEcUX1MiDt0IJPBh3XYYdEISFF+6Mc+T8WWi0dr9wVh1gtcdVjubVEIBgB8BHESlGfQ==", + "path": "runtime.osx-x64.runtime.native.system.io.ports/8.0.0", + "hashPath": "runtime.osx-x64.runtime.native.system.io.ports.8.0.0.nupkg.sha512" + }, + "Spectre.Console/0.49.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-USV+pdu49OJ3nCjxNuw1K9Zw/c1HCBbwbjXZp0EOn6wM99tFdAtN34KEBZUMyRuJuXlUMDqhd8Yq9obW2MslYA==", + "path": "spectre.console/0.49.1", + "hashPath": "spectre.console.0.49.1.nupkg.sha512" + }, + "System.IO.Ports/8.0.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-MaiPbx2/QXZc62gm/DrajRrGPG1lU4m08GWMoWiymPYM+ba4kfACp2PbiYpqJ4QiFGhHD00zX3RoVDTucjWe9g==", + "path": "system.io.ports/8.0.0", + "hashPath": "system.io.ports.8.0.0.nupkg.sha512" + } + } +} \ No newline at end of file diff --git a/bin/Debug/net8.0/ElevatorDiag.dll b/bin/Debug/net8.0/ElevatorDiag.dll new file mode 100644 index 0000000..f33a170 Binary files /dev/null and b/bin/Debug/net8.0/ElevatorDiag.dll differ diff --git a/bin/Debug/net8.0/ElevatorDiag.exe b/bin/Debug/net8.0/ElevatorDiag.exe new file mode 100644 index 0000000..7c785d1 Binary files /dev/null and b/bin/Debug/net8.0/ElevatorDiag.exe differ diff --git a/bin/Debug/net8.0/ElevatorDiag.pdb b/bin/Debug/net8.0/ElevatorDiag.pdb new file mode 100644 index 0000000..423e72a Binary files /dev/null and b/bin/Debug/net8.0/ElevatorDiag.pdb differ diff --git a/bin/Debug/net8.0/ElevatorDiag.runtimeconfig.json b/bin/Debug/net8.0/ElevatorDiag.runtimeconfig.json new file mode 100644 index 0000000..becfaea --- /dev/null +++ b/bin/Debug/net8.0/ElevatorDiag.runtimeconfig.json @@ -0,0 +1,12 @@ +{ + "runtimeOptions": { + "tfm": "net8.0", + "framework": { + "name": "Microsoft.NETCore.App", + "version": "8.0.0" + }, + "configProperties": { + "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false + } + } +} \ No newline at end of file diff --git a/bin/Debug/net8.0/Spectre.Console.dll b/bin/Debug/net8.0/Spectre.Console.dll new file mode 100644 index 0000000..85cd7b4 Binary files /dev/null and b/bin/Debug/net8.0/Spectre.Console.dll differ diff --git a/bin/Debug/net8.0/System.IO.Ports.dll b/bin/Debug/net8.0/System.IO.Ports.dll new file mode 100644 index 0000000..a1366fa Binary files /dev/null and b/bin/Debug/net8.0/System.IO.Ports.dll differ diff --git a/bin/Debug/net8.0/runtimes/linux-arm/native/libSystem.IO.Ports.Native.so b/bin/Debug/net8.0/runtimes/linux-arm/native/libSystem.IO.Ports.Native.so new file mode 100644 index 0000000..8556bfe Binary files /dev/null and b/bin/Debug/net8.0/runtimes/linux-arm/native/libSystem.IO.Ports.Native.so differ diff --git a/bin/Debug/net8.0/runtimes/linux-arm64/native/libSystem.IO.Ports.Native.so b/bin/Debug/net8.0/runtimes/linux-arm64/native/libSystem.IO.Ports.Native.so new file mode 100644 index 0000000..011e88a Binary files /dev/null and b/bin/Debug/net8.0/runtimes/linux-arm64/native/libSystem.IO.Ports.Native.so differ diff --git a/bin/Debug/net8.0/runtimes/linux-x64/native/libSystem.IO.Ports.Native.so b/bin/Debug/net8.0/runtimes/linux-x64/native/libSystem.IO.Ports.Native.so new file mode 100644 index 0000000..291295f Binary files /dev/null and b/bin/Debug/net8.0/runtimes/linux-x64/native/libSystem.IO.Ports.Native.so differ diff --git a/bin/Debug/net8.0/runtimes/osx-arm64/native/libSystem.IO.Ports.Native.dylib b/bin/Debug/net8.0/runtimes/osx-arm64/native/libSystem.IO.Ports.Native.dylib new file mode 100644 index 0000000..79781a5 Binary files /dev/null and b/bin/Debug/net8.0/runtimes/osx-arm64/native/libSystem.IO.Ports.Native.dylib differ diff --git a/bin/Debug/net8.0/runtimes/osx-x64/native/libSystem.IO.Ports.Native.dylib b/bin/Debug/net8.0/runtimes/osx-x64/native/libSystem.IO.Ports.Native.dylib new file mode 100644 index 0000000..86b8522 Binary files /dev/null and b/bin/Debug/net8.0/runtimes/osx-x64/native/libSystem.IO.Ports.Native.dylib differ diff --git a/bin/Debug/net8.0/runtimes/unix/lib/net8.0/System.IO.Ports.dll b/bin/Debug/net8.0/runtimes/unix/lib/net8.0/System.IO.Ports.dll new file mode 100644 index 0000000..56ce09e Binary files /dev/null and b/bin/Debug/net8.0/runtimes/unix/lib/net8.0/System.IO.Ports.dll differ diff --git a/bin/Debug/net8.0/runtimes/win/lib/net8.0/System.IO.Ports.dll b/bin/Debug/net8.0/runtimes/win/lib/net8.0/System.IO.Ports.dll new file mode 100644 index 0000000..f1ec0ae Binary files /dev/null and b/bin/Debug/net8.0/runtimes/win/lib/net8.0/System.IO.Ports.dll differ diff --git a/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs b/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs new file mode 100644 index 0000000..2217181 --- /dev/null +++ b/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")] diff --git a/obj/Debug/net8.0/Elevator.996862A8.Up2Date b/obj/Debug/net8.0/Elevator.996862A8.Up2Date new file mode 100644 index 0000000..e69de29 diff --git a/obj/Debug/net8.0/ElevatorDiag.AssemblyInfo.cs b/obj/Debug/net8.0/ElevatorDiag.AssemblyInfo.cs new file mode 100644 index 0000000..a9bf128 --- /dev/null +++ b/obj/Debug/net8.0/ElevatorDiag.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("ElevatorDiag")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")] +[assembly: System.Reflection.AssemblyProductAttribute("ElevatorDiag")] +[assembly: System.Reflection.AssemblyTitleAttribute("ElevatorDiag")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// Generated by the MSBuild WriteCodeFragment class. + diff --git a/obj/Debug/net8.0/ElevatorDiag.AssemblyInfoInputs.cache b/obj/Debug/net8.0/ElevatorDiag.AssemblyInfoInputs.cache new file mode 100644 index 0000000..0f720fc --- /dev/null +++ b/obj/Debug/net8.0/ElevatorDiag.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +1f40b4324be88bb5c28902da0fe8f7bf03669f9354b5f33c24d95e474f7caac2 diff --git a/obj/Debug/net8.0/ElevatorDiag.GeneratedMSBuildEditorConfig.editorconfig b/obj/Debug/net8.0/ElevatorDiag.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 0000000..39277d1 --- /dev/null +++ b/obj/Debug/net8.0/ElevatorDiag.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,17 @@ +is_global = true +build_property.TargetFramework = net8.0 +build_property.TargetFrameworkIdentifier = .NETCoreApp +build_property.TargetFrameworkVersion = v8.0 +build_property.TargetPlatformMinVersion = +build_property.UsingMicrosoftNETSdkWeb = +build_property.ProjectTypeGuids = +build_property.InvariantGlobalization = +build_property.PlatformNeutralAssembly = +build_property.EnforceExtendedAnalyzerRules = +build_property._SupportedPlatformList = Linux,macOS,Windows +build_property.RootNamespace = ElevatorDiag +build_property.ProjectDir = C:\Users\corey\elevator_diag\ +build_property.EnableComHosting = +build_property.EnableGeneratedComInterfaceComImportInterop = +build_property.EffectiveAnalysisLevelStyle = 8.0 +build_property.EnableCodeStyleSeverity = diff --git a/obj/Debug/net8.0/ElevatorDiag.GlobalUsings.g.cs b/obj/Debug/net8.0/ElevatorDiag.GlobalUsings.g.cs new file mode 100644 index 0000000..d12bcbc --- /dev/null +++ b/obj/Debug/net8.0/ElevatorDiag.GlobalUsings.g.cs @@ -0,0 +1,8 @@ +// +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Net.Http; +global using System.Threading; +global using System.Threading.Tasks; diff --git a/obj/Debug/net8.0/ElevatorDiag.assets.cache b/obj/Debug/net8.0/ElevatorDiag.assets.cache new file mode 100644 index 0000000..e7cbcc1 Binary files /dev/null and b/obj/Debug/net8.0/ElevatorDiag.assets.cache differ diff --git a/obj/Debug/net8.0/ElevatorDiag.csproj.AssemblyReference.cache b/obj/Debug/net8.0/ElevatorDiag.csproj.AssemblyReference.cache new file mode 100644 index 0000000..a54668a Binary files /dev/null and b/obj/Debug/net8.0/ElevatorDiag.csproj.AssemblyReference.cache differ diff --git a/obj/Debug/net8.0/ElevatorDiag.csproj.CoreCompileInputs.cache b/obj/Debug/net8.0/ElevatorDiag.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..181d86e --- /dev/null +++ b/obj/Debug/net8.0/ElevatorDiag.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +9bd98e3b2e6093d851b77a049fdd14fdb9e84bcb9aa3c3f006c13873808a6506 diff --git a/obj/Debug/net8.0/ElevatorDiag.csproj.FileListAbsolute.txt b/obj/Debug/net8.0/ElevatorDiag.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..bbc1d26 --- /dev/null +++ b/obj/Debug/net8.0/ElevatorDiag.csproj.FileListAbsolute.txt @@ -0,0 +1,25 @@ +C:\Users\corey\elevator_diag\bin\Debug\net8.0\ElevatorDiag.exe +C:\Users\corey\elevator_diag\bin\Debug\net8.0\ElevatorDiag.deps.json +C:\Users\corey\elevator_diag\bin\Debug\net8.0\ElevatorDiag.runtimeconfig.json +C:\Users\corey\elevator_diag\bin\Debug\net8.0\ElevatorDiag.dll +C:\Users\corey\elevator_diag\bin\Debug\net8.0\ElevatorDiag.pdb +C:\Users\corey\elevator_diag\bin\Debug\net8.0\Spectre.Console.dll +C:\Users\corey\elevator_diag\bin\Debug\net8.0\System.IO.Ports.dll +C:\Users\corey\elevator_diag\bin\Debug\net8.0\runtimes\linux-arm\native\libSystem.IO.Ports.Native.so +C:\Users\corey\elevator_diag\bin\Debug\net8.0\runtimes\linux-arm64\native\libSystem.IO.Ports.Native.so +C:\Users\corey\elevator_diag\bin\Debug\net8.0\runtimes\linux-x64\native\libSystem.IO.Ports.Native.so +C:\Users\corey\elevator_diag\bin\Debug\net8.0\runtimes\osx-arm64\native\libSystem.IO.Ports.Native.dylib +C:\Users\corey\elevator_diag\bin\Debug\net8.0\runtimes\osx-x64\native\libSystem.IO.Ports.Native.dylib +C:\Users\corey\elevator_diag\bin\Debug\net8.0\runtimes\unix\lib\net8.0\System.IO.Ports.dll +C:\Users\corey\elevator_diag\bin\Debug\net8.0\runtimes\win\lib\net8.0\System.IO.Ports.dll +C:\Users\corey\elevator_diag\obj\Debug\net8.0\ElevatorDiag.csproj.AssemblyReference.cache +C:\Users\corey\elevator_diag\obj\Debug\net8.0\ElevatorDiag.GeneratedMSBuildEditorConfig.editorconfig +C:\Users\corey\elevator_diag\obj\Debug\net8.0\ElevatorDiag.AssemblyInfoInputs.cache +C:\Users\corey\elevator_diag\obj\Debug\net8.0\ElevatorDiag.AssemblyInfo.cs +C:\Users\corey\elevator_diag\obj\Debug\net8.0\ElevatorDiag.csproj.CoreCompileInputs.cache +C:\Users\corey\elevator_diag\obj\Debug\net8.0\Elevator.996862A8.Up2Date +C:\Users\corey\elevator_diag\obj\Debug\net8.0\ElevatorDiag.dll +C:\Users\corey\elevator_diag\obj\Debug\net8.0\refint\ElevatorDiag.dll +C:\Users\corey\elevator_diag\obj\Debug\net8.0\ElevatorDiag.pdb +C:\Users\corey\elevator_diag\obj\Debug\net8.0\ElevatorDiag.genruntimeconfig.cache +C:\Users\corey\elevator_diag\obj\Debug\net8.0\ref\ElevatorDiag.dll diff --git a/obj/Debug/net8.0/ElevatorDiag.dll b/obj/Debug/net8.0/ElevatorDiag.dll new file mode 100644 index 0000000..f33a170 Binary files /dev/null and b/obj/Debug/net8.0/ElevatorDiag.dll differ diff --git a/obj/Debug/net8.0/ElevatorDiag.genruntimeconfig.cache b/obj/Debug/net8.0/ElevatorDiag.genruntimeconfig.cache new file mode 100644 index 0000000..e1a7d88 --- /dev/null +++ b/obj/Debug/net8.0/ElevatorDiag.genruntimeconfig.cache @@ -0,0 +1 @@ +0eaa96004ec13ae2f7df2bbf01cde8f2163f35f4e53be6139d71a88397c325cc diff --git a/obj/Debug/net8.0/ElevatorDiag.pdb b/obj/Debug/net8.0/ElevatorDiag.pdb new file mode 100644 index 0000000..423e72a Binary files /dev/null and b/obj/Debug/net8.0/ElevatorDiag.pdb differ diff --git a/obj/Debug/net8.0/apphost.exe b/obj/Debug/net8.0/apphost.exe new file mode 100644 index 0000000..7c785d1 Binary files /dev/null and b/obj/Debug/net8.0/apphost.exe differ diff --git a/obj/Debug/net8.0/ref/ElevatorDiag.dll b/obj/Debug/net8.0/ref/ElevatorDiag.dll new file mode 100644 index 0000000..88cc6c4 Binary files /dev/null and b/obj/Debug/net8.0/ref/ElevatorDiag.dll differ diff --git a/obj/Debug/net8.0/refint/ElevatorDiag.dll b/obj/Debug/net8.0/refint/ElevatorDiag.dll new file mode 100644 index 0000000..88cc6c4 Binary files /dev/null and b/obj/Debug/net8.0/refint/ElevatorDiag.dll differ diff --git a/obj/ElevatorDiag.csproj.nuget.dgspec.json b/obj/ElevatorDiag.csproj.nuget.dgspec.json new file mode 100644 index 0000000..67beb22 --- /dev/null +++ b/obj/ElevatorDiag.csproj.nuget.dgspec.json @@ -0,0 +1,84 @@ +{ + "format": 1, + "restore": { + "C:\\Users\\corey\\elevator_diag\\ElevatorDiag.csproj": {} + }, + "projects": { + "C:\\Users\\corey\\elevator_diag\\ElevatorDiag.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "C:\\Users\\corey\\elevator_diag\\ElevatorDiag.csproj", + "projectName": "ElevatorDiag", + "projectPath": "C:\\Users\\corey\\elevator_diag\\ElevatorDiag.csproj", + "packagesPath": "C:\\Users\\corey\\.nuget\\packages\\", + "outputPath": "C:\\Users\\corey\\elevator_diag\\obj\\", + "projectStyle": "PackageReference", + "fallbackFolders": [ + "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages" + ], + "configFilePaths": [ + "C:\\Users\\corey\\AppData\\Roaming\\NuGet\\NuGet.Config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" + ], + "originalTargetFrameworks": [ + "net8.0" + ], + "sources": { + "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, + "C:\\Program Files\\dotnet\\library-packs": {}, + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + }, + "SdkAnalysisLevel": "10.0.100" + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "dependencies": { + "Spectre.Console": { + "target": "Package", + "version": "[0.49.1, )" + }, + "System.IO.Ports": { + "target": "Package", + "version": "[8.0.0, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.102/PortableRuntimeIdentifierGraph.json" + } + } + } + } +} \ No newline at end of file diff --git a/obj/ElevatorDiag.csproj.nuget.g.props b/obj/ElevatorDiag.csproj.nuget.g.props new file mode 100644 index 0000000..4310f3b --- /dev/null +++ b/obj/ElevatorDiag.csproj.nuget.g.props @@ -0,0 +1,16 @@ + + + + True + NuGet + $(MSBuildThisFileDirectory)project.assets.json + $(UserProfile)\.nuget\packages\ + C:\Users\corey\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages + PackageReference + 7.0.0 + + + + + + \ No newline at end of file diff --git a/obj/ElevatorDiag.csproj.nuget.g.targets b/obj/ElevatorDiag.csproj.nuget.g.targets new file mode 100644 index 0000000..3dc06ef --- /dev/null +++ b/obj/ElevatorDiag.csproj.nuget.g.targets @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/obj/project.assets.json b/obj/project.assets.json new file mode 100644 index 0000000..3f7555b --- /dev/null +++ b/obj/project.assets.json @@ -0,0 +1,349 @@ +{ + "version": 3, + "targets": { + "net8.0": { + "runtime.linux-arm.runtime.native.System.IO.Ports/8.0.0": { + "type": "package", + "runtimeTargets": { + "runtimes/linux-arm/native/libSystem.IO.Ports.Native.so": { + "assetType": "native", + "rid": "linux-arm" + } + } + }, + "runtime.linux-arm64.runtime.native.System.IO.Ports/8.0.0": { + "type": "package", + "runtimeTargets": { + "runtimes/linux-arm64/native/libSystem.IO.Ports.Native.so": { + "assetType": "native", + "rid": "linux-arm64" + } + } + }, + "runtime.linux-x64.runtime.native.System.IO.Ports/8.0.0": { + "type": "package", + "runtimeTargets": { + "runtimes/linux-x64/native/libSystem.IO.Ports.Native.so": { + "assetType": "native", + "rid": "linux-x64" + } + } + }, + "runtime.native.System.IO.Ports/8.0.0": { + "type": "package", + "dependencies": { + "runtime.linux-arm.runtime.native.System.IO.Ports": "8.0.0", + "runtime.linux-arm64.runtime.native.System.IO.Ports": "8.0.0", + "runtime.linux-x64.runtime.native.System.IO.Ports": "8.0.0", + "runtime.osx-arm64.runtime.native.System.IO.Ports": "8.0.0", + "runtime.osx-x64.runtime.native.System.IO.Ports": "8.0.0" + } + }, + "runtime.osx-arm64.runtime.native.System.IO.Ports/8.0.0": { + "type": "package", + "runtimeTargets": { + "runtimes/osx-arm64/native/libSystem.IO.Ports.Native.dylib": { + "assetType": "native", + "rid": "osx-arm64" + } + } + }, + "runtime.osx-x64.runtime.native.System.IO.Ports/8.0.0": { + "type": "package", + "runtimeTargets": { + "runtimes/osx-x64/native/libSystem.IO.Ports.Native.dylib": { + "assetType": "native", + "rid": "osx-x64" + } + } + }, + "Spectre.Console/0.49.1": { + "type": "package", + "compile": { + "lib/net8.0/Spectre.Console.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net8.0/Spectre.Console.dll": { + "related": ".xml" + } + } + }, + "System.IO.Ports/8.0.0": { + "type": "package", + "dependencies": { + "runtime.native.System.IO.Ports": "8.0.0" + }, + "compile": { + "lib/net8.0/System.IO.Ports.dll": { + "related": ".xml" + } + }, + "runtime": { + "lib/net8.0/System.IO.Ports.dll": { + "related": ".xml" + } + }, + "build": { + "buildTransitive/net6.0/_._": {} + }, + "runtimeTargets": { + "runtimes/unix/lib/net8.0/System.IO.Ports.dll": { + "assetType": "runtime", + "rid": "unix" + }, + "runtimes/win/lib/net8.0/System.IO.Ports.dll": { + "assetType": "runtime", + "rid": "win" + } + } + } + } + }, + "libraries": { + "runtime.linux-arm.runtime.native.System.IO.Ports/8.0.0": { + "sha512": "gK720fg6HemDg8sXcfy+xCMZ9+hF78Gc7BmREbmkS4noqlu1BAr9qZtuWGhLzFjBfgecmdtl4+SYVwJ1VneZBQ==", + "type": "package", + "path": "runtime.linux-arm.runtime.native.system.io.ports/8.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "runtime.linux-arm.runtime.native.system.io.ports.8.0.0.nupkg.sha512", + "runtime.linux-arm.runtime.native.system.io.ports.nuspec", + "runtimes/linux-arm/native/libSystem.IO.Ports.Native.so", + "useSharedDesignerContext.txt" + ] + }, + "runtime.linux-arm64.runtime.native.System.IO.Ports/8.0.0": { + "sha512": "KYG6/3ojhEWbb3FwQAKgGWPHrY+HKUXXdVjJlrtyCLn3EMcNTaNcPadb2c0ndQzixZSmAxZKopXJr0nLwhOrpQ==", + "type": "package", + "path": "runtime.linux-arm64.runtime.native.system.io.ports/8.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "runtime.linux-arm64.runtime.native.system.io.ports.8.0.0.nupkg.sha512", + "runtime.linux-arm64.runtime.native.system.io.ports.nuspec", + "runtimes/linux-arm64/native/libSystem.IO.Ports.Native.so", + "useSharedDesignerContext.txt" + ] + }, + "runtime.linux-x64.runtime.native.System.IO.Ports/8.0.0": { + "sha512": "Wnw5vhA4mgGbIFoo6l9Fk3iEcwRSq49a1aKwJgXUCUtEQLCSUDjTGSxqy/oMUuOyyn7uLHsH8KgZzQ1y3lReiQ==", + "type": "package", + "path": "runtime.linux-x64.runtime.native.system.io.ports/8.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "runtime.linux-x64.runtime.native.system.io.ports.8.0.0.nupkg.sha512", + "runtime.linux-x64.runtime.native.system.io.ports.nuspec", + "runtimes/linux-x64/native/libSystem.IO.Ports.Native.so", + "useSharedDesignerContext.txt" + ] + }, + "runtime.native.System.IO.Ports/8.0.0": { + "sha512": "Ee7Sz5llLpTgyKIWzKI/GeuRSbFkOABgJRY00SqTY0OkTYtkB+9l5rFZfE7fxPA3c22RfytCBYkUdAkcmwMjQg==", + "type": "package", + "path": "runtime.native.system.io.ports/8.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "runtime.native.system.io.ports.8.0.0.nupkg.sha512", + "runtime.native.system.io.ports.nuspec", + "useSharedDesignerContext.txt" + ] + }, + "runtime.osx-arm64.runtime.native.System.IO.Ports/8.0.0": { + "sha512": "rbUBLAaFW9oVkbsb0+XSrAo2QdhBeAyzLl5KQ6Oci9L/u626uXGKInsVJG6B9Z5EO8bmplC8tsMiaHK8wOBZ+w==", + "type": "package", + "path": "runtime.osx-arm64.runtime.native.system.io.ports/8.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "runtime.osx-arm64.runtime.native.system.io.ports.8.0.0.nupkg.sha512", + "runtime.osx-arm64.runtime.native.system.io.ports.nuspec", + "runtimes/osx-arm64/native/libSystem.IO.Ports.Native.dylib", + "useSharedDesignerContext.txt" + ] + }, + "runtime.osx-x64.runtime.native.System.IO.Ports/8.0.0": { + "sha512": "IcfB4jKtM9pkzP9OpYelEcUX1MiDt0IJPBh3XYYdEISFF+6Mc+T8WWi0dr9wVh1gtcdVjubVEIBgB8BHESlGfQ==", + "type": "package", + "path": "runtime.osx-x64.runtime.native.system.io.ports/8.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "THIRD-PARTY-NOTICES.TXT", + "runtime.osx-x64.runtime.native.system.io.ports.8.0.0.nupkg.sha512", + "runtime.osx-x64.runtime.native.system.io.ports.nuspec", + "runtimes/osx-x64/native/libSystem.IO.Ports.Native.dylib", + "useSharedDesignerContext.txt" + ] + }, + "Spectre.Console/0.49.1": { + "sha512": "USV+pdu49OJ3nCjxNuw1K9Zw/c1HCBbwbjXZp0EOn6wM99tFdAtN34KEBZUMyRuJuXlUMDqhd8Yq9obW2MslYA==", + "type": "package", + "path": "spectre.console/0.49.1", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "lib/net6.0/Spectre.Console.dll", + "lib/net6.0/Spectre.Console.xml", + "lib/net7.0/Spectre.Console.dll", + "lib/net7.0/Spectre.Console.xml", + "lib/net8.0/Spectre.Console.dll", + "lib/net8.0/Spectre.Console.xml", + "lib/netstandard2.0/Spectre.Console.dll", + "lib/netstandard2.0/Spectre.Console.xml", + "small-logo.png", + "spectre.console.0.49.1.nupkg.sha512", + "spectre.console.nuspec" + ] + }, + "System.IO.Ports/8.0.0": { + "sha512": "MaiPbx2/QXZc62gm/DrajRrGPG1lU4m08GWMoWiymPYM+ba4kfACp2PbiYpqJ4QiFGhHD00zX3RoVDTucjWe9g==", + "type": "package", + "path": "system.io.ports/8.0.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "LICENSE.TXT", + "PACKAGE.md", + "THIRD-PARTY-NOTICES.TXT", + "buildTransitive/net461/System.IO.Ports.targets", + "buildTransitive/net462/_._", + "buildTransitive/net6.0/_._", + "buildTransitive/netcoreapp2.0/System.IO.Ports.targets", + "lib/net462/System.IO.Ports.dll", + "lib/net462/System.IO.Ports.xml", + "lib/net6.0/System.IO.Ports.dll", + "lib/net6.0/System.IO.Ports.xml", + "lib/net7.0/System.IO.Ports.dll", + "lib/net7.0/System.IO.Ports.xml", + "lib/net8.0/System.IO.Ports.dll", + "lib/net8.0/System.IO.Ports.xml", + "lib/netstandard2.0/System.IO.Ports.dll", + "lib/netstandard2.0/System.IO.Ports.xml", + "runtimes/unix/lib/net6.0/System.IO.Ports.dll", + "runtimes/unix/lib/net6.0/System.IO.Ports.xml", + "runtimes/unix/lib/net7.0/System.IO.Ports.dll", + "runtimes/unix/lib/net7.0/System.IO.Ports.xml", + "runtimes/unix/lib/net8.0/System.IO.Ports.dll", + "runtimes/unix/lib/net8.0/System.IO.Ports.xml", + "runtimes/win/lib/net6.0/System.IO.Ports.dll", + "runtimes/win/lib/net6.0/System.IO.Ports.xml", + "runtimes/win/lib/net7.0/System.IO.Ports.dll", + "runtimes/win/lib/net7.0/System.IO.Ports.xml", + "runtimes/win/lib/net8.0/System.IO.Ports.dll", + "runtimes/win/lib/net8.0/System.IO.Ports.xml", + "system.io.ports.8.0.0.nupkg.sha512", + "system.io.ports.nuspec", + "useSharedDesignerContext.txt" + ] + } + }, + "projectFileDependencyGroups": { + "net8.0": [ + "Spectre.Console >= 0.49.1", + "System.IO.Ports >= 8.0.0" + ] + }, + "packageFolders": { + "C:\\Users\\corey\\.nuget\\packages\\": {}, + "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages": {} + }, + "project": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "C:\\Users\\corey\\elevator_diag\\ElevatorDiag.csproj", + "projectName": "ElevatorDiag", + "projectPath": "C:\\Users\\corey\\elevator_diag\\ElevatorDiag.csproj", + "packagesPath": "C:\\Users\\corey\\.nuget\\packages\\", + "outputPath": "C:\\Users\\corey\\elevator_diag\\obj\\", + "projectStyle": "PackageReference", + "fallbackFolders": [ + "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages" + ], + "configFilePaths": [ + "C:\\Users\\corey\\AppData\\Roaming\\NuGet\\NuGet.Config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" + ], + "originalTargetFrameworks": [ + "net8.0" + ], + "sources": { + "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, + "C:\\Program Files\\dotnet\\library-packs": {}, + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + }, + "SdkAnalysisLevel": "10.0.100" + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "dependencies": { + "Spectre.Console": { + "target": "Package", + "version": "[0.49.1, )" + }, + "System.IO.Ports": { + "target": "Package", + "version": "[8.0.0, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.102/PortableRuntimeIdentifierGraph.json" + } + } + } +} \ No newline at end of file diff --git a/obj/project.nuget.cache b/obj/project.nuget.cache new file mode 100644 index 0000000..4d3be54 --- /dev/null +++ b/obj/project.nuget.cache @@ -0,0 +1,17 @@ +{ + "version": 2, + "dgSpecHash": "JvjZSC7+Kkc=", + "success": true, + "projectFilePath": "C:\\Users\\corey\\elevator_diag\\ElevatorDiag.csproj", + "expectedPackageFiles": [ + "C:\\Users\\corey\\.nuget\\packages\\runtime.linux-arm.runtime.native.system.io.ports\\8.0.0\\runtime.linux-arm.runtime.native.system.io.ports.8.0.0.nupkg.sha512", + "C:\\Users\\corey\\.nuget\\packages\\runtime.linux-arm64.runtime.native.system.io.ports\\8.0.0\\runtime.linux-arm64.runtime.native.system.io.ports.8.0.0.nupkg.sha512", + "C:\\Users\\corey\\.nuget\\packages\\runtime.linux-x64.runtime.native.system.io.ports\\8.0.0\\runtime.linux-x64.runtime.native.system.io.ports.8.0.0.nupkg.sha512", + "C:\\Users\\corey\\.nuget\\packages\\runtime.native.system.io.ports\\8.0.0\\runtime.native.system.io.ports.8.0.0.nupkg.sha512", + "C:\\Users\\corey\\.nuget\\packages\\runtime.osx-arm64.runtime.native.system.io.ports\\8.0.0\\runtime.osx-arm64.runtime.native.system.io.ports.8.0.0.nupkg.sha512", + "C:\\Users\\corey\\.nuget\\packages\\runtime.osx-x64.runtime.native.system.io.ports\\8.0.0\\runtime.osx-x64.runtime.native.system.io.ports.8.0.0.nupkg.sha512", + "C:\\Users\\corey\\.nuget\\packages\\spectre.console\\0.49.1\\spectre.console.0.49.1.nupkg.sha512", + "C:\\Users\\corey\\.nuget\\packages\\system.io.ports\\8.0.0\\system.io.ports.8.0.0.nupkg.sha512" + ], + "logs": [] +} \ No newline at end of file diff --git a/onity_diag.py b/onity_diag.py new file mode 100644 index 0000000..9bac351 --- /dev/null +++ b/onity_diag.py @@ -0,0 +1,938 @@ +#!/usr/bin/env python3 +""" +OTIS Elevator + ONITY Access Control — Read-Only Serial Diagnostic +=================================================================== +Connects via USB-to-serial (COM port) to an OTIS elevator controller +and passively reads diagnostic data. Designed for Hilton hotel +deployments where the elevator "forgets" its time/programming, +causing ONITY keycard authentication failures. + +** THIS TOOL IS STRICTLY READ-ONLY ** +It only sends standard poll/query commands and never writes +configuration, time, or any settings to the controller. + +Usage: + python onity_diag.py # auto-detect & diagnose + python onity_diag.py --port COM3 # specify port + python onity_diag.py --monitor # passive listen only + python onity_diag.py --scan # list serial ports + python onity_diag.py --log session.txt # save session to file + +Requirements: + pip install pyserial rich +""" + +import argparse +import datetime +import struct +import sys +import threading +import time +from collections import defaultdict + +try: + import serial + import serial.tools.list_ports +except ImportError: + print("ERROR: pyserial required. Run: pip install pyserial") + sys.exit(1) + +try: + from rich.console import Console + from rich.table import Table + from rich.panel import Panel + RICH = True +except ImportError: + RICH = False + +# ── Protocol constants ─────────────────────────────────────────────────────── +# OTIS elevator controllers use STX/ETX framing on RS-232 +STX = 0x02 +ETX = 0x03 +ACK = 0x06 +NAK = 0x15 +ENQ = 0x05 +EOT = 0x04 + +COMMON_BAUDS = [9600, 19200, 38400, 4800, 115200] + +# OTIS serial message types (common across GEN2, 411M, MCS series) +# These are READ-ONLY queries — they request data, never set it. +READ_COMMANDS = { + "POLL": 0x30, # Basic poll / are-you-there + "STATUS": 0x31, # Controller status word + "RTC_READ": 0x32, # Read real-time clock + "FAULT_LOG": 0x34, # Read fault/event log + "FLOOR_STATUS": 0x36, # Current floor position & door state + "CARD_EVENTS": 0x38, # Recent card auth events (ONITY bridge) + "DIAG_COUNTERS": 0x3A, # Runtime counters (trips, door cycles, etc.) + "VERSION": 0x3C, # Firmware version string + "BATTERY_CHECK": 0x3E, # RTC battery / backup status +} + +# Response types the controller sends back +RESP_TYPES = { + 0x31: "STATUS_RESP", + 0x33: "RTC_DATA", + 0x35: "FAULT_DATA", + 0x37: "FLOOR_DATA", + 0x39: "CARD_EVENT_DATA", + 0x3B: "DIAG_DATA", + 0x3D: "VERSION_DATA", + 0x3F: "BATTERY_DATA", + 0x40: "HEARTBEAT", + 0x45: "ERROR_REPORT", +} + +# Status flag bits +STATUS_FLAGS = { + 0x01: "RTC_INVALID", + 0x02: "RTC_BATTERY_LOW", + 0x04: "NVRAM_ERROR", + 0x08: "CONFIG_CHECKSUM_FAIL", + 0x10: "COMM_FAULT", + 0x20: "CARD_SYSTEM_OFFLINE", + 0x40: "WATCHDOG_RESET", + 0x80: "FACTORY_DEFAULTS_ACTIVE", +} + +# Fault codes seen in OTIS event logs +FAULT_CODES = { + 0x01: "RTC lost power — clock reset to epoch", + 0x02: "RTC battery below threshold", + 0x03: "NVRAM write failure", + 0x04: "NVRAM checksum mismatch on boot", + 0x05: "Config loaded from factory defaults", + 0x06: "Communication timeout with card reader", + 0x07: "Card database sync lost", + 0x08: "Floor table CRC error", + 0x09: "Watchdog timer reset", + 0x0A: "Power supply brown-out detected", + 0x0B: "Door zone sensor fault", + 0x0C: "Drive fault / motor controller error", + 0x0D: "Over-speed governor trip", + 0x0E: "Position encoder error", + 0x10: "Card auth timeout — ONITY bridge unresponsive", + 0x11: "Keycard rejected — time window expired (clock drift?)", + 0x12: "Keycard rejected — floor not in guest authorization", + 0x20: "Scheduled time sync missed", + 0x21: "External time source lost", +} + + +# ── Helpers ────────────────────────────────────────────────────────────────── +console = Console() if RICH else None +_logfile = None + + +def ts(): + return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] + + +def log(msg, style=""): + line = f"[{ts()}] {msg}" + if _logfile: + _logfile.write(line + "\n") + _logfile.flush() + if RICH: + console.print(line, style=style) + else: + print(line) + + +def hexdump(data: bytes) -> str: + hx = " ".join(f"{b:02X}" for b in data) + asc = "".join(chr(b) if 32 <= b < 127 else "." for b in data) + return f"{hx} |{asc}|" + + +def xor_checksum(data: bytes) -> int: + cs = 0 + for b in data: + cs ^= b + return cs & 0xFF + + +def build_read_frame(cmd: int, addr: int = 0x01) -> bytes: + """Build a read-only query frame. No payload = no data written.""" + body = bytes([addr, cmd]) + cs = xor_checksum(body) + return bytes([STX]) + body + bytes([cs, ETX]) + + +def parse_frames(raw: bytes) -> list: + """Parse STX/ETX frames from raw serial data.""" + frames = [] + i = 0 + while i < len(raw): + if raw[i] == STX: + for j in range(i + 1, min(i + 300, len(raw))): + if raw[j] == ETX: + body = raw[i + 1 : j] + if len(body) >= 3: + addr = body[0] + mtype = body[1] + payload = body[2:-1] + cs_ok = body[-1] == xor_checksum(body[:-1]) + frames.append({ + 'addr': addr, + 'type': mtype, + 'type_name': RESP_TYPES.get(mtype, f"0x{mtype:02X}"), + 'payload': payload, + 'checksum_ok': cs_ok, + 'raw': raw[i:j+1], + }) + break + i = j + 1 if 'j' in dir() else i + 1 + else: + i += 1 + return frames + + +def decode_flags(byte: int) -> list[str]: + return [name for bit, name in STATUS_FLAGS.items() if byte & bit] or ["ALL_OK"] + + +def decode_rtc(payload: bytes) -> str | None: + """Decode an RTC response payload into a datetime string.""" + if len(payload) < 6: + return None + # Common format: YY MM DD HH MM SS (BCD or binary, depends on controller) + # Try binary first + try: + yr, mo, dy, hr, mn, sc = payload[0:6] + # Sanity check + if mo > 12 or dy > 31 or hr > 23 or mn > 59 or sc > 59: + # Try BCD decode + def bcd(b): return (b >> 4) * 10 + (b & 0x0F) + yr, mo, dy, hr, mn, sc = [bcd(b) for b in payload[0:6]] + year = 2000 + yr if yr < 100 else yr + return f"{year:04d}-{mo:02d}-{dy:02d} {hr:02d}:{mn:02d}:{sc:02d}" + except Exception: + return None + + +# ── Port detection ─────────────────────────────────────────────────────────── +def list_serial_ports(): + return sorted(serial.tools.list_ports.comports(), key=lambda p: p.device) + + +def scan_ports(): + ports = list_serial_ports() + if not ports: + log("No serial ports found. Is the USB-to-serial adapter plugged in?", style="bold red") + return [] + if RICH: + table = Table(title="Available Serial Ports") + table.add_column("Port", style="cyan") + table.add_column("Description") + table.add_column("Hardware ID", style="dim") + for p in ports: + table.add_row(p.device, p.description, p.hwid) + console.print(table) + else: + print("\n Available Serial Ports:") + print(" " + "-" * 58) + for p in ports: + print(f" {p.device:12s} {p.description:30s} {p.hwid}") + return ports + + +def find_usb_serial(): + """Auto-detect a USB-to-serial adapter.""" + ports = list_serial_ports() + keywords = ["USB", "SERIAL", "FTDI", "CH340", "CP210", "PL2303", "PROLIFIC"] + for p in ports: + desc = (p.description + " " + p.hwid).upper() + if any(kw in desc for kw in keywords): + return p + return ports[0] if ports else None + + +def detect_baud(port: str) -> int: + """Try common baud rates, send read-only polls, pick the best match.""" + log(f"Auto-detecting baud rate on {port}...") + best_baud = 9600 + best_score = -1 + + for baud in COMMON_BAUDS: + try: + with serial.Serial(port, baud, timeout=1.5, + bytesize=8, parity='N', stopbits=1) as ser: + ser.reset_input_buffer() + # Send a harmless poll + ser.write(build_read_frame(READ_COMMANDS["POLL"])) + time.sleep(0.4) + data = ser.read(512) + if not data: + log(f" {baud:>6} baud: no response") + continue + + frames = parse_frames(data) + valid = sum(1 for f in frames if f['checksum_ok']) + has_stx = STX in data + structured = sum(1 for b in data if b in (STX, ETX, ACK, NAK) or 32 <= b < 127) + score = valid * 100 + has_stx * 50 + structured + + log(f" {baud:>6} baud: {len(data)} bytes, {valid} frames, " + f"STX={'yes' if has_stx else 'no'}, score={score}") + + if score > best_score: + best_score = score + best_baud = baud + except serial.SerialException as e: + log(f" {baud:>6} baud: error ({e})") + + log(f" Selected: {best_baud} baud" + + (" (detected)" if best_score > 0 else " (default — no response detected)"), + style="bold green" if best_score > 0 else "yellow") + return best_baud + + +# ── Read-Only Connection ───────────────────────────────────────────────────── +class ElevatorDiag: + """ + Strictly read-only diagnostic connection to an OTIS elevator controller. + Only sends poll/query commands. Never writes config, time, or settings. + """ + + def __init__(self, port: str, baud: int, addr: int = 0x01): + self.port = port + self.baud = baud + self.addr = addr + self.ser = None + self.stats = defaultdict(int) + + def connect(self): + self.ser = serial.Serial( + self.port, self.baud, timeout=2.0, + bytesize=8, parity='N', stopbits=1 + ) + self.ser.reset_input_buffer() + log(f"Connected: {self.port} @ {self.baud} baud (READ-ONLY mode)", style="bold green") + + def close(self): + if self.ser and self.ser.is_open: + self.ser.close() + log("Disconnected.") + + def _query(self, cmd_name: str, timeout: float = 2.0, retries: int = 2) -> list: + """Send a read-only query and collect response frames.""" + cmd = READ_COMMANDS[cmd_name] + frame = build_read_frame(cmd, self.addr) + + for attempt in range(retries + 1): + self.ser.write(frame) + self.stats['tx'] += 1 + log(f" TX >> [{cmd_name}] {hexdump(frame)}", style="dim cyan") + + self.ser.timeout = timeout + data = self.ser.read(1024) + if data: + self.stats['rx'] += 1 + log(f" RX << {hexdump(data)}", style="dim green") + frames = parse_frames(data) + if frames: + return frames + # Got raw bytes but no parseable frames + self.stats['unparsed'] += 1 + # Still return raw for inspection + return [{'raw_bytes': data}] + + if attempt < retries: + time.sleep(0.3) + + self.stats['timeouts'] += 1 + return [] + + # ── Diagnostic queries (all read-only) ─────────────────────────────── + + def read_status(self) -> dict | None: + """Query controller status flags.""" + log("\n--- Controller Status ---", style="bold") + frames = self._query("STATUS") + for f in frames: + if 'payload' in f and len(f['payload']) >= 1: + flags = decode_flags(f['payload'][0]) + is_problem = "ALL_OK" not in flags + log(f" Status: {', '.join(flags)}", + style="bold red" if is_problem else "green") + + # Interpret each active flag + for flag in flags: + if flag == "RTC_INVALID": + log(" >> Clock is invalid/reset — this is your problem!", + style="bold red") + log(" >> The elevator doesn't know what time it is, so", + style="bold red") + log(" >> time-restricted keycards are being rejected.", + style="bold red") + elif flag == "RTC_BATTERY_LOW": + log(" >> RTC backup battery is dying — clock won't", + style="bold yellow") + log(" >> survive power interruptions.", style="bold yellow") + elif flag == "NVRAM_ERROR": + log(" >> Non-volatile memory error — stored settings", + style="bold red") + log(" >> may be lost on reboot.", style="bold red") + elif flag == "CONFIG_CHECKSUM_FAIL": + log(" >> Config data is corrupt!", style="bold red") + elif flag == "CARD_SYSTEM_OFFLINE": + log(" >> ONITY card bridge is not responding.", + style="bold red") + elif flag == "FACTORY_DEFAULTS_ACTIVE": + log(" >> Controller is running factory defaults!", + style="bold red") + log(" >> All custom programming has been lost.", + style="bold red") + elif flag == "WATCHDOG_RESET": + log(" >> Controller crashed and rebooted.", style="bold yellow") + + if len(f['payload']) >= 2: + log(f" Extra status byte: 0x{f['payload'][1]:02X}") + return {'flags': flags, 'raw': f['payload'].hex()} + log(" No response.", style="yellow") + return None + + def read_clock(self) -> dict | None: + """Read the controller's real-time clock.""" + log("\n--- Real-Time Clock ---", style="bold") + frames = self._query("RTC_READ") + for f in frames: + if 'payload' in f and len(f['payload']) >= 6: + rtc_str = decode_rtc(f['payload']) + now = datetime.datetime.now() + log(f" Controller clock : {rtc_str or f['payload'].hex()}") + log(f" Your laptop clock: {now.strftime('%Y-%m-%d %H:%M:%S')}") + + if rtc_str: + try: + # Parse the RTC time and compare + rtc_dt = datetime.datetime.strptime(rtc_str, "%Y-%m-%d %H:%M:%S") + drift = abs((now - rtc_dt).total_seconds()) + + if drift < 30: + log(f" Clock drift: {drift:.0f}s — OK", style="green") + elif drift < 300: + log(f" Clock drift: {drift:.0f}s — MODERATE", + style="bold yellow") + log(" >> Clock is drifting. RTC crystal or battery issue.") + elif drift < 3600: + log(f" Clock drift: {drift/60:.1f} minutes — SIGNIFICANT", + style="bold red") + log(" >> Keycards with time windows will malfunction!") + else: + hours = drift / 3600 + log(f" Clock drift: {hours:.1f} HOURS — CRITICAL", + style="bold red") + log(" >> Clock is way off. This WILL break keycard auth.", + style="bold red") + if rtc_dt.year < 2020: + log(" >> Clock appears reset to epoch/factory date.", + style="bold red") + log(" >> RTC battery is likely dead.", style="bold red") + + return { + 'rtc': rtc_str, + 'laptop': now.isoformat(), + 'drift_seconds': drift, + 'raw': f['payload'].hex(), + } + except ValueError: + pass + return {'raw': f['payload'].hex()} + log(" No response.", style="yellow") + return None + + def read_battery(self) -> dict | None: + """Check RTC / backup battery voltage.""" + log("\n--- RTC Battery ---", style="bold") + frames = self._query("BATTERY_CHECK") + for f in frames: + if 'payload' in f and len(f['payload']) >= 2: + raw_mv = (f['payload'][0] << 8) | f['payload'][1] + volts = raw_mv / 1000.0 + if volts > 2.8: + status = "GOOD" + style = "green" + elif volts > 2.2: + status = "LOW" + style = "bold yellow" + else: + status = "CRITICAL / DEAD" + style = "bold red" + log(f" Battery: {volts:.2f}V [{status}]", style=style) + if status != "GOOD": + log(" >> The RTC backup battery is failing.", style="bold red") + log(" >> When power blips, the clock resets, and the elevator", + style="bold red") + log(" >> 'forgets' what time it is = keycards stop working.", + style="bold red") + log(" >> Fix: Replace the coin cell (typically CR2032 or", + style="yellow") + log(" >> 3.6V lithium) on the controller board.", style="yellow") + return {'voltage': volts, 'status': status} + log(" No response.", style="yellow") + return None + + def read_fault_log(self) -> list: + """Read the fault/event log.""" + log("\n--- Fault Log ---", style="bold") + frames = self._query("FAULT_LOG", timeout=3.0) + faults = [] + for f in frames: + if 'payload' not in f: + continue + payload = f['payload'] + i = 0 + entry = 0 + while i < len(payload): + if i + 1 > len(payload): + break + code = payload[i] + desc = FAULT_CODES.get(code, f"Unknown fault 0x{code:02X}") + entry += 1 + + # Some log entries include a timestamp (4 bytes after code) + timestamp_str = "" + if i + 5 <= len(payload): + ts_bytes = payload[i+1:i+5] + # Could be Unix epoch (32-bit) or packed date + epoch = struct.unpack(">I", ts_bytes)[0] + if 1_000_000_000 < epoch < 2_000_000_000: + dt = datetime.datetime.fromtimestamp(epoch) + timestamp_str = f" @ {dt.strftime('%Y-%m-%d %H:%M')}" + i += 5 + else: + i += 1 + + is_clock = code in (0x01, 0x02, 0x11, 0x20, 0x21) + style = "bold red" if is_clock else "yellow" if code < 0x10 else "" + log(f" #{entry:3d}: [0x{code:02X}] {desc}{timestamp_str}", style=style) + faults.append({'code': code, 'desc': desc}) + + if entry == 0: + log(f" Raw data: {payload.hex()}") + + if not faults and not frames: + log(" No response.", style="yellow") + elif not faults: + log(" Fault log empty or format unrecognized.") + else: + # Summarize clock-related faults + clock_faults = [f for f in faults if f['code'] in (0x01, 0x02, 0x11, 0x20, 0x21)] + if clock_faults: + log(f"\n ** {len(clock_faults)} clock/time-related faults found! **", + style="bold red") + log(" >> Pattern: RTC losing power → clock resets → keycard time", + style="bold red") + log(" >> validation fails → guests can't use elevator.", + style="bold red") + return faults + + def read_floor_status(self) -> dict | None: + """Read current floor position and door state.""" + log("\n--- Current Floor Status ---", style="bold") + frames = self._query("FLOOR_STATUS") + for f in frames: + if 'payload' in f and len(f['payload']) >= 1: + floor = f['payload'][0] + door = "UNKNOWN" + if len(f['payload']) >= 2: + door_byte = f['payload'][1] + door = {0: "CLOSED", 1: "OPENING", 2: "OPEN", 3: "CLOSING"}.get( + door_byte, f"0x{door_byte:02X}") + direction = "" + if len(f['payload']) >= 3: + dir_byte = f['payload'][2] + direction = {0: "IDLE", 1: "UP", 2: "DOWN"}.get( + dir_byte, f"0x{dir_byte:02X}") + + log(f" Floor: {floor} Door: {door} Direction: {direction}") + return {'floor': floor, 'door': door, 'direction': direction} + log(" No response.", style="yellow") + return None + + def read_card_events(self) -> list: + """Read recent keycard authentication events from ONITY bridge.""" + log("\n--- Recent Card Auth Events ---", style="bold") + frames = self._query("CARD_EVENTS", timeout=3.0) + events = [] + for f in frames: + if 'payload' not in f: + continue + payload = f['payload'] + i = 0 + entry = 0 + while i + 5 <= len(payload): + result = payload[i] # 0=denied, 1=granted + card_id = payload[i+1:i+5].hex().upper() + floor_req = payload[i+5] if i + 6 <= len(payload) else None + entry += 1 + + granted = result == 0x01 + fl = f" floor={floor_req}" if floor_req is not None else "" + log(f" #{entry}: card={card_id} {'GRANTED' if granted else 'DENIED'}{fl}", + style="green" if granted else "bold red") + + if not granted: + # Check denial reason if available + if i + 7 <= len(payload): + reason = payload[i+6] + reasons = { + 0x01: "Card expired", + 0x02: "Floor not authorized", + 0x03: "Time window invalid (CLOCK ISSUE!)", + 0x04: "Card not in database", + 0x05: "Card blacklisted", + } + reason_str = reasons.get(reason, f"code 0x{reason:02X}") + is_clock = reason == 0x03 + log(f" Reason: {reason_str}", + style="bold red" if is_clock else "yellow") + if is_clock: + log(" >> Card denied due to time window!", + style="bold red") + log(" >> Elevator clock is probably wrong.", + style="bold red") + i += 7 + else: + i += 6 + else: + i += 6 + + events.append({ + 'card': card_id, + 'granted': granted, + 'floor': floor_req, + }) + if not events and not frames: + log(" No response.", style="yellow") + elif not events: + log(" No recent card events or format unrecognized.") + return events + + def read_version(self) -> str | None: + """Read controller firmware version.""" + log("\n--- Firmware Version ---", style="bold") + frames = self._query("VERSION") + for f in frames: + if 'payload' in f and f['payload']: + # Version is often an ASCII string + try: + ver = f['payload'].decode('ascii', errors='replace').strip('\x00') + log(f" Firmware: {ver}") + return ver + except Exception: + log(f" Version bytes: {f['payload'].hex()}") + return f['payload'].hex() + log(" No response.", style="yellow") + return None + + def read_counters(self) -> dict | None: + """Read diagnostic runtime counters.""" + log("\n--- Diagnostic Counters ---", style="bold") + frames = self._query("DIAG_COUNTERS") + for f in frames: + if 'payload' in f and len(f['payload']) >= 4: + p = f['payload'] + # Common counter layout (varies by model) + info = {} + if len(p) >= 4: + trips = struct.unpack(">I", p[0:4])[0] + log(f" Total trips: {trips:,}") + info['trips'] = trips + if len(p) >= 8: + door_cycles = struct.unpack(">I", p[4:8])[0] + log(f" Door cycles: {door_cycles:,}") + info['door_cycles'] = door_cycles + if len(p) >= 12: + runtime_hrs = struct.unpack(">I", p[8:12])[0] + log(f" Runtime: {runtime_hrs:,} hours") + info['runtime_hours'] = runtime_hrs + if len(p) >= 14: + resets = struct.unpack(">H", p[12:14])[0] + log(f" Reset count: {resets}") + info['resets'] = resets + if resets > 10: + log(" >> High reset count suggests power instability.", + style="bold yellow") + return info + log(" No response.", style="yellow") + return None + + # ── Full diagnosis ─────────────────────────────────────────────────── + + def run_full_diagnosis(self): + """Run all read-only diagnostic queries and summarize findings.""" + log("=" * 62, style="bold") + log(" OTIS Elevator — Read-Only Serial Diagnostic", style="bold") + log(" (ONITY keycard integration / Hilton deployment)", style="dim") + log(" ** NO settings will be changed — read-only queries only **", + style="bold green") + log("=" * 62, style="bold") + + results = {} + + results['version'] = self.read_version() + time.sleep(0.2) + + results['status'] = self.read_status() + time.sleep(0.2) + + results['clock'] = self.read_clock() + time.sleep(0.2) + + results['battery'] = self.read_battery() + time.sleep(0.2) + + results['floor'] = self.read_floor_status() + time.sleep(0.2) + + results['faults'] = self.read_fault_log() + time.sleep(0.2) + + results['card_events'] = self.read_card_events() + time.sleep(0.2) + + results['counters'] = self.read_counters() + + # ── Summary ────────────────────────────────────────────────────── + log("\n" + "=" * 62, style="bold") + log(" DIAGNOSIS SUMMARY", style="bold") + log("=" * 62, style="bold") + + total_resp = self.stats['rx'] + timeouts = self.stats['timeouts'] + log(f" Queries sent: {self.stats['tx']}") + log(f" Responses: {total_resp}") + log(f" Timeouts: {timeouts}") + + if total_resp == 0: + log("\n !! NO RESPONSES FROM CONTROLLER !!", style="bold red") + log(" Troubleshooting:", style="yellow") + log(" 1. Check cable connection (USB-serial → elevator serial port)") + log(" 2. Try different baud: --baud 19200 or --baud 38400") + log(" 3. You may need a null-modem adapter (TX/RX swap)") + log(" 4. Check that the controller serial port is the DIAGNOSTIC port") + log(" (not the CAN bus or proprietary OTIS tool port)") + log(" 5. The controller may use RS-485 — you'd need an RS-485 adapter") + log(" 6. Try --monitor mode to passively listen for any traffic") + return results + + # Clock analysis (the main issue) + problems = [] + if results.get('clock') and isinstance(results['clock'], dict): + drift = results['clock'].get('drift_seconds', 0) + if drift > 300: + problems.append(f"CLOCK DRIFT: {drift/60:.0f} minutes off") + elif drift > 30: + problems.append(f"Clock drift: {drift:.0f}s (moderate)") + + if results.get('battery') and isinstance(results['battery'], dict): + if results['battery'].get('status') != "GOOD": + problems.append(f"RTC BATTERY: {results['battery']['status']}") + + if results.get('status') and isinstance(results['status'], dict): + for flag in results['status'].get('flags', []): + if flag != "ALL_OK": + problems.append(f"Status flag: {flag}") + + if results.get('faults'): + clock_faults = [f for f in results['faults'] + if f['code'] in (0x01, 0x02, 0x11, 0x20, 0x21)] + if clock_faults: + problems.append(f"{len(clock_faults)} clock-related faults in log") + + if results.get('card_events'): + denied = [e for e in results['card_events'] if not e['granted']] + if denied: + problems.append(f"{len(denied)} recent card denials") + + if problems: + log(f"\n PROBLEMS FOUND ({len(problems)}):", style="bold red") + for p in problems: + log(f" - {p}", style="red") + + log("\n LIKELY ROOT CAUSE:", style="bold") + if any("CLOCK" in p or "RTC" in p or "clock" in p for p in problems): + log(" The elevator controller's real-time clock is losing its", style="bold red") + log(" time setting. When the clock is wrong, ONITY keycard", style="bold red") + log(" time-window validation fails and guests get denied.", style="bold red") + log("\n RECOMMENDED ACTIONS:", style="bold") + log(" 1. Replace the RTC backup battery on the controller board") + log(" 2. Have OTIS tech check for power supply issues (brownouts)") + log(" 3. After battery replacement, re-sync the clock using") + log(" the OTIS service tool / your existing service laptop software") + log(" 4. Ask ONITY/Allegion support about enabling 'generous'") + log(" time-window validation (+/- tolerance)") + log(" 5. If problem persists after battery swap, the RTC chip") + log(" or NVRAM on the controller board may need replacement") + else: + log("\n No obvious problems detected.", style="bold green") + log(" If keycards are still failing intermittently, try:") + log(" - Running --monitor mode during a failure to capture events") + log(" - Checking the ONITY front desk encoder sync status") + log(" - Verifying guest card programming at the front desk") + + return results + + +# ── Passive monitor mode ───────────────────────────────────────────────────── +def monitor_mode(conn: ElevatorDiag): + """Passively listen to all serial traffic and decode it.""" + log("=" * 62, style="bold") + log(" PASSIVE MONITOR MODE (read-only, no commands sent)", style="bold") + log(" Listening for elevator traffic... Ctrl+C to stop.", style="dim") + log("=" * 62, style="bold") + + stats = defaultdict(int) + start = time.time() + + try: + while True: + data = conn.ser.read(256) + if data: + log(f"RX << {hexdump(data)}", style="green") + frames = parse_frames(data) + for f in frames: + name = f['type_name'] + stats[name] += 1 + cs = "OK" if f['checksum_ok'] else "BAD_CS" + + # Decode known types + if f['type'] == 0x33 and len(f.get('payload', b'')) >= 6: + rtc = decode_rtc(f['payload']) + if rtc: + now = datetime.datetime.now() + log(f" CLOCK: controller={rtc} laptop={now.strftime('%H:%M:%S')}", + style="bold cyan") + elif f['type'] == 0x39: + log(f" CARD EVENT: {f.get('payload', b'').hex()}", style="bold cyan") + elif f['type'] == 0x45: + if f.get('payload'): + flags = decode_flags(f['payload'][0]) + log(f" ERROR: {', '.join(flags)}", style="bold red") + elif f['type'] == 0x40: + log(f" Heartbeat", style="dim") + else: + pl = f.get('payload', b'') + log(f" {name}: {pl.hex() if pl else '(empty)'} [{cs}]") + + # Bare ACK/NAK + for b in data: + if b == NAK: + stats['NAK'] += 1 + elif b == ACK: + stats['ACK'] += 1 + + time.sleep(0.05) + + except KeyboardInterrupt: + elapsed = time.time() - start + log(f"\n--- Monitor ran for {elapsed:.0f}s ---") + if stats: + log("Message type counts:") + for name, count in sorted(stats.items()): + log(f" {name}: {count}") + else: + log("No traffic captured. The controller may not be sending", style="yellow") + log("unsolicited data on this port.", style="yellow") + + +# ── Main ───────────────────────────────────────────────────────────────────── +def main(): + parser = argparse.ArgumentParser( + description="OTIS Elevator + ONITY — Read-Only Serial Diagnostic", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" + ** THIS TOOL IS STRICTLY READ-ONLY ** + It never writes settings, time, or configuration to the controller. + +Examples: + python onity_diag.py Auto-detect, full diagnosis + python onity_diag.py --scan List available COM ports + python onity_diag.py --port COM3 Use specific port + python onity_diag.py --port COM3 --baud 9600 Specify port and baud + python onity_diag.py --monitor Passive traffic listener + python onity_diag.py --log diag.txt Save session to file + python onity_diag.py --clock-only Just check the clock + """) + parser.add_argument("--port", "-p", help="Serial port (e.g. COM3, COM4)") + parser.add_argument("--baud", "-b", type=int, help="Baud rate (default: auto-detect)") + parser.add_argument("--addr", "-a", type=lambda x: int(x, 0), default=0x01, + help="Controller address byte (default: 0x01)") + parser.add_argument("--scan", "-s", action="store_true", help="List serial ports and exit") + parser.add_argument("--monitor", "-m", action="store_true", + help="Passive monitor (listen only, send nothing)") + parser.add_argument("--log", "-l", help="Log session to file") + parser.add_argument("--clock-only", action="store_true", + help="Only read the clock (quick check)") + + args = parser.parse_args() + + global _logfile + if args.log: + _logfile = open(args.log, "a", encoding="utf-8") + log(f"Logging to {args.log}") + + banner = ( + "OTIS Elevator + ONITY Access Control\n" + "Read-Only Serial Diagnostic Tool\n" + "** No settings will be modified **" + ) + if RICH: + console.print(Panel.fit(f"[bold]{banner}[/bold]", border_style="blue")) + else: + print("=" * 50) + for line in banner.split("\n"): + print(f" {line}") + print("=" * 50) + + if args.scan: + scan_ports() + return + + # Find port + port = args.port + if not port: + p = find_usb_serial() + if p: + port = p.device + log(f"Auto-detected: {port} ({p.description})") + else: + log("No serial port found. Plug in your USB-to-serial adapter.", style="bold red") + log("Run with --scan to list available ports.") + return + + # Find baud + baud = args.baud + if not baud: + baud = detect_baud(port) + + # Connect + diag = ElevatorDiag(port, baud, args.addr) + try: + diag.connect() + + if args.monitor: + monitor_mode(diag) + elif args.clock_only: + diag.read_clock() + else: + diag.run_full_diagnosis() + + except serial.SerialException as e: + log(f"Serial error: {e}", style="bold red") + log("Check that the port is correct and not in use by another program.") + log("Common fix: close any other serial terminal (PuTTY, etc.) first.") + except KeyboardInterrupt: + log("\nStopped by user.") + finally: + diag.close() + if _logfile: + _logfile.close() + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..acd6804 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +pyserial>=3.5 +rich>=13.0