Files
cimtechniques-service-suite/docs/VB6-MIGRATION-PLAYBOOK.md
2026-06-02 11:14:37 -04:00

16 KiB
Raw Blame History

VB6 → Modern Migration Playbook

A reusable guide for modernizing legacy Visual Basic 6 applications, distilled from the DA-12 Service Tool rebuild (VB6 → Python 3 + PySide6). Copy this file into each new migration project and adapt the app-specific parts.

The DA-12 tool is used throughout as the worked example, but the process, the VB6 quirk reference, and the packaging/testing patterns are general and apply to any VB6 desktop app — especially ones that talk to hardware, instruments, or a serial/ USB/TCP device.


1. Why these apps break (and what "done" means)

VB6 apps stop running on modern Windows not because the logic is bad but because the runtime and UI components are gone:

  • VB6 runtime (MSVBVM60.dll) — not present by default on modern Windows.
  • 32-bit ActiveX/OCX controls that must be regsvr32-registered with admin rights. Common culprits: VSFLEX7L.OCX/VSFLEX8 (grids), VSOCX6.OCX (layout/tabs), MSCOMM32.OCX (serial), RICHTX32.OCX (rich text), COMDLG32.OCX, MSWINSCK.OCX (sockets), TABCTL32.OCX.
  • Proprietary/vendor DLLs or type libraries (.tlb/.dll) the app references.

"Done" therefore means: a self-contained app that installs and launches with no runtime/OCX registration, reproducing the workflows users know. Treat clean deployment as a first-class deliverable, not a final-day step — it's usually the entire reason for the rebuild.

Key early question for every app: does the vendor DLL/tlb do real work, or just helper functions? On the DA-12, cimscan.tlb was referenced but only provided time/format/hex helpers — none of the device I/O depended on it. Confirming this early (grep every reference, check call sites) determines whether you have a clean-room rebuild or a hard COM-interop problem.


2. Choosing the target stack

Two stacks cover almost every VB6 desktop app. Pick per project; document the call.

Python + PySide6 (Qt) C# / .NET (WPF or WinUI)
Best when The owner wants to read/tweak it; team is Python-ish; no dedicated devs Deployment bulletproof-ness is paramount; .NET skills available
Serial/HW pyserial (excellent) System.IO.Ports (excellent)
Grids/tabs QTableWidget/QTabWidget map ~1:1 onto VSFlexGrid/vsElastic DataGrid/TabControl
Deployment PyInstaller one-folder + Inno Setup; watch AV false-positives, larger bundles dotnet publish self-contained single-file — gold standard, minimal AV friction
Maintainability for a non-dev owner High (readable) Lower (can't read C#)

Capability is not the deciding factor — both do everything a VB6 line-of-business or instrument tool needs. The real trade-off is maintainability by whoever owns it vs deployment robustness. For CIMTechniques (PM owner, Python-literate, no in-house devs) we chose Python + PySide6 and made packaging a tracked deliverable to neutralize its one weakness. If a future app is safety-critical or distributed to many external customers with zero support tolerance, reconsider .NET for that one.


3. The migration process (phased)

This order front-loads risk (the protocol/business logic) and keeps the app runnable without the real device the whole way through.

  1. Reverse-engineer & spec. Read the .frm/.bas/.cls source; document how it talks to its device/DB/peers and what every screen shows. Write a design spec. Explicitly list what you're dropping with source locations (see §7).
  2. Pure core first (no I/O, no UI). Encode the wire/file/DB format as small, fully unit-tested functions. This is where reverse-engineering errors surface.
  3. Swappable transport + a simulator. Put the real I/O behind one interface and write a fake that speaks the same protocol. Now the whole app runs with no device.
  4. Domain layer. Models + a controller that wires transport → core → models and exposes high-level operations + change signals.
  5. UI. Thin views that render models and call controller methods on edit.
  6. Packaging + docs + external verification. Build the installer; write a hardware/DB verification checklist for the things only the real environment can confirm.

Commit per step, keep tests green, and keep a REBUILD-STATUS.md so anyone (or a future session) can pick up.


4. Reading VB6 source

File types:

  • .vbp — project file. Lists Object= (OCX deps with GUIDs), Reference= (DLL/tlb deps), forms/modules, and version/metadata. Start here — it tells you the whole dependency surface.
  • .frm — a form: a UI-definition header (controls, properties, geometry in twips) followed by the event-handler code. The code is after the Attribute VB_Name = "..." line.
  • .frx — binary blob for a form (icons, images). Not human-readable.
  • .bas — standard module (shared procedures, Declare Win32 APIs, Type defs, globals).
  • .cls — class module.
  • .vbw — window layout (ignore). .tmp/backup copies of forms may linger.

Where the logic lives: event handlers (Sub Control_Event()), plus Public/ Private Subs/Functions. For an instrument app, find the comms event handler (e.g. MSComm1_OnComm) and the send routines — that's the protocol.

Practical tips: grep for (Private|Public) (Sub|Function) to map the procedures; grep vendor-DLL function names across all files to see if they're load-bearing; the UI-definition portion of large .frm files is verbose noise — skip to the code.


5. VB6 quirks → modern equivalents (the reusable gold)

These are the gotchas that cause silent wrong-data bugs if mishandled. Verified on the DA-12 job.

Numeric & hex parsing

VB6 Behavior Python equivalent / trap
CInt("&H" & s) Parse hex as signed 16-bit. &HFFFF = 1, &H8000 = 32768 v=int(s,16)&0xFFFF; v-0x10000 if v&0x8000 else v. Don't use plain int(s,16)
CLng("&H" & s) Parse hex as signed 32-bit signed-32 helper; &HFFFFFFFF = 1
Int(x) Floors toward −∞ (1.5 → 2) Python int() truncates toward zero! Use math.floor — matters for negative values
Fix(x) Truncates toward zero Python int()
Val(s) Lenient leading-numeric parse, never errors write a tolerant parser; float() raises
Hex$(n) Uppercase hex, no padding format(n & MASK, "0NX") for fixed width (VB code often hand-pads via Hex2/Hex4/Hex8)
Round(x, n) Banker's rounding (round-half-to-even) Python round() is also banker's — matches. Format$ may differ; verify

Strings (all 1-based!)

VB6 Note Python
Mid$(s, start, len) 1-based start s[start-1:start-1+len]
Left$(s,n) / Right$(s,n) s[:n] / s[-n:]
InStr(start, s, sub) 1-based, returns 0 if not found s.find(sub, start-1)+1 semantics — be careful
Trim$/LTrim$/RTrim$ .strip()/.lstrip()/.rstrip()
s$, n%, l&, x!, d# type-suffix: String, Integer(16), Long(32), Single, Double dataclass fields / annotations

Fixed-point & sentinels (instrument protocols)

  • VB6 instrument code typically stores reals as scaled integers: stored = real × 10^n, transmitted as hex. Decode = signed(hex) / 10^n. Each field can have a different scale — map them individually from the parse routine.
  • Watch for NaN/"no value" sentinels like &H8000 (32768) or a literal "FFFF8000". CAUTION: the same byte pattern may be a sentinel in one routine and a real value in another. On the DA-12, the value/limit fields treated 8000 as "no value", but the scale/offset fields (AddFloat) did not8000 there is a legitimate 3.2768. Check every parse helper's sentinel logic separately; don't globalize it.

Control flow & errors

VB6 Meaning Python
On Error Resume Next Swallow errors, keep going targeted try/except; don't blanket-swallow
On Error GoTo Label Jump to handler try/except block
DoEvents in a Do…Loop Busy-wait pumping the UI use real async / a reader thread / QTimer
Type … End Type record struct @dataclass
Declare Function … Lib "user32" Win32 API call ctypes, or a stdlib/Qt equivalent (often unnecessary)

UI & components

VB6 / OCX Replacement (Qt)
VSFlexGrid (grid) QTableWidget / QTableView + model
vsElastic / SSTab / TabCtl (layout/tabs) Qt layouts + QTabWidget
MSComm (serial OCX, _OnComm events) pyserial in a reader thread → signal
RichTextBox (RICHTX32) QTextBrowser / QTextEdit
MSWinsock (TCP) socket / QTcpSocket
Control arrays (txt(0), txt(1)) a Python list/dict of widgets
Predeclared form instances (frmMain global, PredeclaredId) explicit object passed in, not a global
Geometry in twips (1/1440") ignore; use Qt layouts. (1 twip ≈ 1/15 px at 96 DPI)
App.Path bundle-aware path; user data via QStandardPaths
Binary settings files (.tab via Put/Get) JSON config

Serial specifics (very common in these apps)

  • Port settings string "19200,n,8,1" → pyserial Serial(port, 19200, bytesize=8, parity='N', stopbits=1, timeout=0.1). Mirror DTR/RTS if the VB set them.
  • Decode bytes as latin-1 if the protocol is ASCII-hex + control chars (lossless byte↔char) rather than utf-8.
  • _OnComm fires on received data; replicate with a daemon reader thread that appends to a buffer and pushes complete frames out.

6. Architecture pattern that worked

Four layers; the protocol/business core is pure and the device is swappable.

UI (Qt views)  ──signals──>  Controller (orchestration, QObject)
                                   │ uses
            Pure core (framing/parse/encode/codecs — NO I/O, 100% tested)
                                   │ bytes
            Transport interface ──> RealTransport (pyserial/socket/db)
                                └─> Simulator (same interface, fake device)

Why it pays off:

  • Pure core = the reverse-engineering is testable with vectors derived from the VB6 source, before any hardware exists.
  • Simulator = the whole UI runs with no device (demo, CI, headless tests). Make it deterministic (no randomness/realtime in test paths).
  • One swappable interface means the real transport and the simulator are interchangeable, and the GUI never knows which it's talking to.

Qt threading tip: route the reader thread's data into the controller via a Qt signal connected with AutoConnection (the default): it delivers synchronously when emitted from the GUI thread (so the simulator and tests need no event loop) and queues onto the GUI thread when emitted from the reader thread (thread-safe). One line, both cases handled.

Edit-loop tip: when a table view repaints from the model, block its itemChanged signal during repopulation so programmatic fills don't fire phantom "user edited" events.


7. Document what you DROP (don't just delete)

Legacy apps accumulate obsolete features. When the owner confirms something is dead (on the DA-12: wireless sensors, an annunciator panel, a debug tab, factory-only serial-number writing), don't silently omit it — write a "Dropped / Deferred Features" section in the spec with, for each item:

  • what it did,
  • the exact legacy source locations (file:line for the handler, the UI controls, the inbound parse branch, the outbound command),
  • a one-line "how to restore" note.

This makes scope cuts auditable and cheap to reverse, and it protects you when "we don't use that anymore" turns out to be wrong six months later.


8. Testing strategy

  • Unit-test the pure core hard. Hand-compute expected values from the VB6 logic and assert them. The signed-hex / scaling / sentinel rules (§5) are exactly where bugs hide — cover them explicitly.
  • Integration-test through the simulator. Drive the controller with the fake device and assert round-trips (set a value → read it back).
  • Headless UI smoke tests with pytest-qt under QT_QPA_PLATFORM=offscreen: instantiate each tab/dialog, feed model data, assert it renders and that edits call the controller.
  • Run a code review on the protocol layer before trusting it — a second pass caught two silent wrong-data bugs on the DA-12 (the sentinel and Int()-floor issues above) that all looked plausible.
  • You usually won't have ground-truth captures (the DA-12 repo's Exceptions.txt was just a crash log). The first real frames you capture from the device become your most valuable regression fixtures — save them.

9. Packaging & deployment (the actual point)

  • PyInstaller one-folder build (not --onefile): faster start and fewer AV false-positives than one-file. Exclude unused heavy modules (e.g. PySide6's QtQml/QtQuick/QtWebEngine/Multimedia) to shrink the bundle.
  • Inno Setup installer: per-machine install to Program Files, Start-menu shortcut, no runtime/OCX registration. (Compiler ISCC.exe must be installed.)
  • Antivirus / SmartScreen: unsigned PyInstaller exes get flagged — this is the #1 "customer can't run it" risk and exactly what you're trying to escape. Plan for an Authenticode code-signing certificate for both the exe and the installer.
  • Test on a clean Windows VM with no Python/dev tools — that's the only way to catch missing-dependency failures before the customer does.
  • Data/config locations: use QStandardPaths (AppData/AppConfig) for config (JSON, replacing binary .tab/.ini), logs, and history files — never write next to the exe in Program Files.

10. External-dependency verification

Some things genuinely can't be confirmed from source — device clock epoch/format, firmware enums, exact register/message maps, DB quirks. Isolate each unknown to a small, clearly-commented spot (a single codec/helper) and write a HARDWARE-VERIFICATION.md (or ENV-VERIFICATION.md) checklist: what to do, expected result, and the exact fix-up location if it's wrong. This lets the owner finish verification without spelunking the codebase.


10b. Consolidating multiple apps into one suite

If you're modernizing several VB6 apps for the same users on the same machines, strongly consider a unified app with each tool as a module on a shared core, rather than N separate apps. Customers then install/sign/update one thing — which is usually the whole point of the modernization.

Decision guide:

  • Share a core package regardless (transport base, codec primitives, UI table base, config, logging, packaging/signing). This is pure upside.
  • One unified shell when tools share users/machines, have no access boundaries, and can share a release cadence. Separate shells on the shared core when audiences, access levels, or update rhythms differ. (Same code reuse either way; the difference is runtime coupling.)
  • Don't over-build the framework from one app. Extract only the obviously-generic pieces into core; let shared abstractions emerge with the 2nd/3rd module (rule of three). A thin module contract (id, title, create_widget(), shutdown(), each module owning its own device connection) is enough to start.

The DA-12 project is taking this path — see docs/SUITE-ARCHITECTURE.md for the concrete target structure, module contract, and sequencing.

11. Per-app reuse checklist

  • Read .vbp; inventory OCX (Object=) and DLL/tlb (Reference=) deps.
  • Determine whether any vendor DLL/tlb does load-bearing work (grep call sites).
  • Map procedures; locate the comms/DB handler and send/parse routines.
  • Choose stack (Python+PySide6 default; .NET if deployment-criticality demands).
  • Write spec incl. Dropped/Deferred Features with source locations.
  • Build pure core + tests (mind §5 signed-hex/scale/sentinel/Int traps).
  • Build transport interface + simulator.
  • Build domain (models + controller w/ AutoConnection signals).
  • Build thin UI; block edit signals on repopulate.
  • PyInstaller one-folder + Inno Setup; exclude unused modules.
  • Code-signing cert; clean-VM install test.
  • *-VERIFICATION.md checklist for environment-only unknowns.
  • REBUILD-STATUS.md handoff.

Source project: DA-12 Service Tool rebuild — see its docs/superpowers/specs/, docs/superpowers/plans/, docs/HARDWARE-VERIFICATION.md, and the cim_suite/modules/da12/ package for concrete, working examples of every pattern above.