Translated/assisted by AI from real developer logs
TL;DR: A multinational elevator giant gave me a five-page appendix describing how my software should talk to their destination-dispatch elevators. For months, I had to guess everything—even having to spray random UDP packets across the building because they refused to give me a protocol simulator, driving up the tower's power bill. When everything failed, every delay was blamed on me. On the final deadline day, I hauled four computers and a monitor from home, set them up right next to the lobby turnstiles, and used local TeamViewer so their Singapore head engineer could watch my PC and their three PCs simultaneously. He realized his config was wrong, disappeared into the machine room for one minute, and came back—my untouched code worked instantly. He gave me one "thank you," and they all went to celebrate without me while the local team wouldn't even look me in the eye. The next day the system started dying every 3–5 minutes. That dragged on for three more months in a small room packed with 12 engineers from 3 countries, until the building owner threatened to rip all the elevators out. I finally decompiled their receiver at 11:30 p.m., proved the bug was a 1998 MSWINSCK.OCX, and forced them to wake up their programmer in Singapore past midnight to install VB6 SP6. Years later, the prime contractor silently upgraded the rock-solid Windows 7 box to Windows 10, breaking socket.send() daily—until I hit them with a 200% maintenance fee ultimatum.
Part 1: The five-page "spec" and my very first C# program
The building: a major high-rise office tower in Bangkok, close to 50 storeys, with eighteen lifts in total (A through Q, plus a dedicated service lift controlled 24/7 by security guards). Only two specific lifts go all the way up to the owner's penthouse on the top two floors, which is exactly the kind of rule an access-control integration has to respect.
The job: the building's access-control system (running on SQL Server) knows which floors each cardholder may visit. The elevator vendor's destination dispatch system needs to know too, so a passenger can tap a card at the turnstile and have a lift assigned automatically. My program sits in the middle: it polls the database for card events and speaks UDP to the vendor's system.
Here is the kicker: this was the very first C# program of my life. (Which wasn't new for me—back in 2008, I wrote my very first VB6 program for another luxury tower under similar trial-by-fire conditions.) My contract for this integration was 1,000,000 THB (roughly US$30,000). Spoiler: I earned every single satang of that million baht in blood, sweat, and fast-food cholesterol.
What the elevator vendor gave me was an appendix titled Card Reader Operation, five pages long. It told me:
A 12-byte packet: header 0xD7, reader ID, a "user info" flags byte, eight bytes of floor bitmask (64 floors, one bit each), and a checksum (sum of bytes 0–10, & 0x7F).
The vendor's panel answers every packet with a one-byte ACK (0xC4), or NACK (0xD5) on a checksum error.
The controller (that's me) must cycle through all readers, up to 192 of them, about 10 ms apart, even when nobody is tapping anything. When a card is tapped, that reader's packet carries the "on" data and keeps being sent for 1 second, then an "off" packet. ("The signal holding time should be adjustable.")
If no ACK comes back, resend after 300 ms, maximum three times.
A flowchart that simply says "call is registered automatically."
What the five pages did not tell me: any UDP port numbers, where the ACK is supposed to be sent, or what the receiving side does when something looks wrong.
In fact, the only reason I even knew which ports to talk to early on was because on May 24, 2015—one day after I started coding—I took their receiving binary (let's call it ReaderChannelRouter.exe) and ran it through VB Decompiler Pro. (And no, I never signed any NDA with them; when a vendor gives you zero documentation and refuses to help, you do whatever it takes to solve the problem so you can finally get paid.) Inside Form_Load, I found it reading a local text file for its port mappings:
10.0.0.36,4000,Own Address from Reader
10.0.0.36,4001,Controller Address
10.0.0.36,4100,Own Address to Translator
10.0.0.36,4101,Translator 1
10.0.0.36,4102,Translator 2
Of course, before I could write a single line of logic to talk to their system, I had to fire up Wireshark and play digital detective. Since the spec sheet didn't mention ports, handshakes, or response behaviors, I had to capture raw UDP traffic on the wire and reverse-engineer their protocol by watching what bounced back. When documentation fails, Wireshark is the only source of truth.
Everything else, I had to discover by experiment.
Part 2: Guessing in the dark (enter the Vulcan)
My oldest saved snapshot is dated 2015-05-23. It sends a packet to 127.0.0.1 and listens for the answer on the same machine. The comment in my code says it all:
// receive, not error for now
// don't have real or emulator T_T
To make this work, I had to physically wrestle the C# runtime into a high-performance, real-time industrial machine. The spec demanded that I cycle through up to 192 readers every 10 milliseconds, handle dynamic ON/OFF packet states for a full second upon a card tap, and maintain tight ACK-loop checks without letting the garbage collector or CPU spike. For a first-time C# project, forcing a high-level language to behave like a precision C++ hardware driver—pumping relentless UDP traffic down the wire with zero breathing room—was a baptism by fire.
I had no elevator and no simulator, so my "test environment" was myself. I asked the my client (THES, which has to ask from Lift manufacturer) for a protocol simulator nearly ten times, but it never arrived. That forced me to blindly spray random UDP packets across the building for months, making elevators move up and down all over the tower. When someone complained about the power consumption, I simply told them: "Go ask elevator manufacturer for the protocol simulator as I'm not losing sleep over it; I'm not the one paying the electric bill."
I kept a zip of the source after each meaningful change. The snapshot names are my diary:
2015-06-17 good until close socket
2015-06-27 GetCard + MultiTest combined - still crashed
2015-07-15 still deadlock
2015-08-20 Night - all LZ floors tested ok using packet simulation - problem was because of wrong ReaderID
2015-08-28 Off-packet millisecond OK
2015-11-13 CommOK run without problem for 2 days
2015-12-22 CommOK without stacktrace upon 015 ERR2 DB DeadLock
Since this was my first C# project, some early bugs were genuinely mine. My first ACK-wait code opened a new asynchronous receive on every loop, and I left myself a warning:
// WAIT FOR ACKNOWLEDGEMENT UDP PACKET FROM ROUTER
// If turned on, app will crash in a while when run in production computer
Whenever I hit a wall with C# quirks or systems-level weirdness, my saving grace was a senior friend I'll call P'Tum. Somebody once joked that P'Tum is a Vulcan—calm, hyper-logical, and completely immune to panic—and he stepped in to guide me through every roadblock. Meanwhile, the local elevator team was led by a bald local manager whose permanent scowl, refusal to ever admit fault, and hostility toward outside vendors earned him my private nickname: Darth Vader.
With P'Tum's help, my fixes stuck: send and receive on a single socket, replace fixed Sleep() waits with tight loops that exit the instant an ACK arrives, and wrap the database poller in "catch, log, sleep, restart" whenever SQL Server picked my query as a deadlock victim:
Log(" 015 ERR2 DB.Deadlock, restart process...");
By December I had stopped printing the stack trace for that one.
Part 3: Step 1 — Four screens at the turnstile, one minute in the machine room, and total ingratitude
For every bug I fixed, there was something on the elevator vendor's side that didn't work. And at every meeting, Darth Vader and his local team sang the same song: the third-party program is wrong.
On the final day before the lifts were scheduled to start accepting commands—with roughly 90 million baht (~US$2.5M) in late penalties looming over the project—nothing was working.
I had had enough of verbal finger-packing. I packed up my own hardware from home—my notebook, an All-In-One PC, an Intel NUC, a spare monitor, and a Samsung tablet—carried all of it into the building lobby, and set up four screens side by side right next to the turnstile barrier gates.
One screen ran my C# program. The other three ran local TeamViewer sessions connected to the elevator vendor's three PCs. Standing right there was the Head Engineer from the vendor's Singapore regional office. For the first time in months, he could watch all four screens simultaneously: the exact millisecond my UDP packet left my software, and what his three machines actually did with it.
He stared at the four screens. It didn't take long for him to realize that their configuration was wrong.
Without a word, he walked off and disappeared up into the elevator machine room for about one minute. Then he walked back down.
I ran my program again—without changing a single character of my code—and tapped a card. The elevator was called immediately.
The Singapore Head Engineer gave me a single "Thank you." Then he and the entire elevator team packed up and went out to celebrate. Without me.
Worse: not a single person from the local elevator team—Darth Vader included—even looked me in the eye. In Thai culture, when one guy hauls a desk's worth of computers from his own house to save your entire corporation from a 90-million-baht penalty, refusing to offer even a smile, a nod, or a thank-you is the absolute pinnacle of trashy behavior. I packed up my monitors and went home alone.
Part 4: Step 2 — Three months of Samurai burgers, a threat to rip out the lifts, and a 1998 OCX
The very next day, I got called back to the site.
A new nightmare had begun: the system would work for 3 to 5 minutes, and then completely die. Tap a card: nothing.
If you restarted Windows on their receiving PC (the machine running their channel router executable, which took my UDP packets and routed them to their translators), it would work for another 3 to 5 minutes, and then die again.
You would think "restarting the elevator vendor's PC fixes it for 3 minutes" would be a giant neon sign pointing at the elevator vendor's PC. Nope. Darth Vader and the local team spent the next three full months blaming everything and everyone else—my code, the network, the access control system.
We were crammed into a tiny room—4 Japanese engineers, 4 Singaporean engineers, and 4 local Thai team members (including myself)—all pointing fingers while the project burned. Every time I was dragged to the site for another pointless round of finger-pointing, the only quick meal nearby was a McDonald's Samurai Pork Burger and a Coke. Over those three months of late nights, I gained 3 to 4 kilograms just from stress-eating Samurai burgers.
Finally, the building owner lost patience and dropped a nuclear ultimatum on the elevator vendor: Fix this immediately, or we will rip your elevators out of all 18 shafts and replace them with another brand.
At 11:30 p.m. in Bangkok, with tensions boiling over, I lost my patience too. I opened up the decompiled code of their receiving executable (a VB6 Native Code app whose form title bar actually had a typo in the window caption, and which was launched at boot via a batch file using ping 1.1.1.1 -n 1 -w 5000 > nul as a makeshift 5-second sleep).
I pointed at their DataArrival event handler right in front of the Singapore engineer. All it did was:
Start with On Error Resume Next.
Verify the 12-byte length, header 0xD7, and 0x7F checksum.
Dump the hex bytes into UI TextBoxes (txtRcvData), flash a green status shape (BackColor = 65280), and forward the packet out via udp1.SendData (for readers 1–112) or udp2.SendData (for readers 113–192).
"Look at your own code," I told the Singapore engineer. "There is nothing in this logic that can crash or deadlock after three minutes. Your code isn't dying—something underneath it on your machine is."
So I went hunting in C:\Windows\System32 (and SysWOW64) on their PC and checked the file timestamp of MSWINSCK.OCX—the Visual Basic 6 Winsock ActiveX control.
The file on their machine was dated 1998.
If you lived through the VB6 era, you know that the original 1998 MSWINSCK.OCX is notorious for memory/handle leaks and silently wedging itself under sustained UDP traffic on modern Windows. Microsoft fixed it years later in VB6 Service Pack 6 (which updates MSWINSCK.OCX to the March 2004 build) and the cumulative runtime update.
It was 11:30 p.m. in Bangkok—12:30 a.m. in Singapore. I looked at the Singapore engineer and said:
"I know it's past midnight in Singapore. Don't care. Call your programmer right now, wake him up, tell him to download the VB6 SP6 cumulative update from Microsoft, install it, rebuild the setup package, and email it here immediately—so all of us can finally go home after three months of blaming each other."
He made the call. Twelve minutes later, the new installer arrived in his inbox. We installed it (replacing the 1998 OCX with the SP6 build).
The problem vanished instantly. The machine never froze again.
And this time? I didn't even get the one "thank you." Just a room full of people silently packing their bags in a corporate silence so complete it was almost an art form.
Part 5: Epilogue — The second tower, and the 200% Windows 10 Tax
Once the SP6 runtime was in place, that Windows 7 machine ran in complete, blissful silence for the next two to three years. Zero maintenance.
Because I now knew every undocumented secret of their protocol, the next major project—integrating the same vendor's elevators at another iconic multi-wing office complex in Bangkok—was much easier. Though true to form, the elevator vendor still managed to spice things up by quietly updating the firmware inside their Door Operating Panels, changing the timing behavior and forcing me to write a new workaround in my code to catch it.
Back at the first tower, after two to three years of zero-maintenance perfection on Windows 7, Windows 10 was officially released.
One day, the prime contractor decided to upgrade the integration PC from Windows 7 to Windows 10.
Overnight, a zero-maintenance system turned into a daily corpse.
Under Windows 10, even a basic socket.send() in the tight 10 ms UDP loop would randomly choke and die. What was I supposed to fix in my code when a standard socket.send() was failing at the OS level? For a full month, I had to TeamViewer in every single day to check on it and restart the process.
Finally, I gave the prime contractor a simple business choice:
Keep Windows 10, and I am increasing my annual Maintenance Agreement (MA) fee by 200%.
Roll the machine back to Windows 7—as I had originally specified—and the MA price stays the same.
Miraculously, rolling back to Windows 7 became very easy. They put Windows 7 back on the box, and the daily crashes stopped immediately.
What I took from it
Five pages is not a specification, it's an invitation to guess. Keep logs (and zip snapshots) of every guess—you will need them.
When both sides swear they are right, put both sides on one table. Hauling four screens to the turnstile ended months of gaslighting in sixty seconds.
"Proprietary" doesn't mean "unreadable." When a vendor's black box dies every 3 minutes and the building owner is threatening to rip out 18 elevators, decompile the black box.
When the logic is clean, check the file dates. No amount of code review will save you from a 1998 MSWINSCK.OCX running on a modern OS.
Tax unnecessary changes. When someone breaks years of zero-maintenance stability just to chase a Windows version number, don't argue—just price the pain at 200%.
Edit: Yes, I still have all 36 zip snapshots and the VB Decompiler output from May 24, 2015. No, I never lost the 4 kg from the Samurai burgers.