
简介这份资源是面向测绘与测量仪器二次开发者的徕卡TPS1200全站仪GeoCOM开发实例包适合具备C基础、希望用Visual C控制全站仪进行数据采集与参数设置的工程师和初学者。压缩包共23个文件约159KB包含5个h头文件、4个cpp源文件及hpp、dsw、dsp等工程文件另有lib静态库、dll动态链接库、exe示例程序与rc资源脚本构成一套可直接编译运行的VC工程。其中GComS2K120.dll与配套lib封装了设备初始化、测量数据读取、参数配置等关键函数com_pub.hpp与头文件则给出协议操作码、数据结构与通信流程说明示例对话框代码演示了命令发送与响应接收的完整链路。目前已有361人学习下载读者可借此理解GeoCOM通信协议结构掌握在VC中引用DLL、处理通信超时与数据格式错误的方法并以此为起点快速搭建自己的全站仪控制程序。1. 从 TPS1200_GeoCOM_samples_VC.rar 说起全站仪二次开发到底在做什么如果你手里有一台徕卡 TPS1200 系列全站仪又恰好需要在 Windows 上写一个自动测量程序那你大概率绕不开 GeoCOM。这个压缩包的名字已经把三件事说清楚了设备是 TPS1200接口是 GeoCOM示例代码是 VCVisual C。它解决的核心问题是——不让测量员一遍遍手动照准、记录而是让上位机通过串口或 TCP 把指令发下去把坐标和角度收回来。我第一次接触这套东西是在一个变形监测的小项目里当时的需求很朴素每 30 分钟自动测一轮 8 个棱镜点存成 CSV。听起来简单但真上手才发现GeoCOM 的坑不在协议本身而在“怎么把 ASCII 指令拼对、怎么把返回值解析对、怎么处理超时和断线”。这个包里的 VC 示例价值就在于它把这条链路跑通了你能照着改。这篇文章面向两类人一类是刚拿到仪器、想用 C 做自动化的新手另一类是用过 GeoCOM 但总在通信层翻车的熟手。我会从协议结构讲到最小可跑代码再讲参数怎么设、坑在哪最后给一个能直接用的轮询测量框架。2. GeoCOM 协议拆解请求、响应与 TPS1200 的指令集2.1 为什么是 ASCII 而不是二进制GeoCOM 有两种模式低级模式ASCII和高级模式二进制。TPS1200 的示例代码几乎都用 ASCII原因很实际——你能用串口助手直接敲指令调试不用先写一个解析器。ASCII 请求的格式是%R1Q,RPC号,参数1,参数2,...CRLF响应格式是%R1P,返回值,结果1,结果2,...CRLF%R1Q表示请求%R1P表示响应。RPC 号是 GeoCOM 的函数编号比如 2023 是TMC_GetSimpleMea获取简单测量值17017 是AUT_MakePositioning自动照准。参数之间用逗号分隔结尾必须是回车换行。这里有个容易忽略的点TPS1200 的串口默认参数是 19200 波特率、8 数据位、无校验、1 停止位。如果你用 115200 去连仪器根本不理你。我见过有人调了一下午最后发现是波特率错了。2.2 返回值不是 0 就代表成功吗GeoCOM 的返回值定义在geocom.h里0 表示GRC_OK其他都是错误码。但这里有个陷阱有些函数返回 0 只代表“指令被接受”不代表“测量完成”。比如TMC_GetSimpleMea的返回值是 0但结果里的坐标可能是上一次的缓存值。正确的做法是看第二个返回值——TMC_GetSimpleMea的响应里有一个rc字段它才是真正的测量状态。如果rc不是 0说明测量失败可能是棱镜被挡住或者仪器没照准。// 解析 TMC_GetSimpleMea 的响应 // 响应格式: %R1P,0,0,rc,hz,v,slopeDist,east,north,height int ParseSimpleMea(const std::string resp, double hz, double v, double dist) { // 去掉 %R1P, 前缀 std::string data resp.substr(5); std::vectorstd::string parts; std::stringstream ss(data); std::string item; while (std::getline(ss, item, ,)) { parts.push_back(item); } // parts[0] 是 RPC 返回值parts[1] 是测量状态 rc if (parts.size() 8) return -1; int rc std::stoi(parts[1]); if (rc ! 0) return rc; // 测量失败 hz std::stod(parts[2]); v std::stod(parts[3]); dist std::stod(parts[4]); return 0; }这段代码的关键是parts[1]它对应响应里的第二个字段。很多人只检查parts[0]结果拿到一堆无效数据还不知道为什么。2.3 自动照准和测量是两步操作TPS1200 的自动测量流程不是一条指令搞定的。你需要先调用AUT_MakePositioning让仪器转到目标角度再调用TMC_GetSimpleMea触发测量。这两步之间要留时间——仪器转动需要几百毫秒到几秒取决于角度差。常见做法是发完AUT_MakePositioning后轮询AUT_GetAtmStatus或者直接Sleep(1000)。我一般会用一个循环检查TMC_GetSimpleMea的rc如果返回的是“正在测量”就继续等。// 自动照准并测量带重试 int AutoMeasure(double targetHz, double targetV, double outHz, double outV, double outDist) { char cmd[256]; // 第一步转动到目标角度 sprintf(cmd, %%R1Q,17017,%f,%f,0,0\r\n, targetHz, targetV); SendCommand(cmd); std::string resp ReadResponse(); if (resp.find(%R1P,0,0) std::string::npos) { return -1; // 转动失败 } // 等待仪器稳定 Sleep(1500); // 第二步触发测量最多重试 3 次 for (int i 0; i 3; i) { sprintf(cmd, %%R1Q,2023,1,0\r\n); // 1 表示精测模式 SendCommand(cmd); resp ReadResponse(); int rc ParseSimpleMea(resp, outHz, outV, outDist); if (rc 0) return 0; Sleep(500); } return -2; // 测量失败 }17017的参数依次是水平角、垂直角、定位模式、等待时间。定位模式 0 表示“转到指定角度”1 表示“转到并跟踪”。等待时间设 0 表示不等待直接返回。3. 在 VC 里跑通第一个 GeoCOM 程序串口配置与最小代码3.1 串口打开失败先查这三个地方VC 里操作串口有两种方式Windows API 和第三方库。示例代码通常用 API因为不依赖外部 DLL。打开串口的代码大概长这样// 打开串口返回句柄 HANDLE OpenComPort(int portNum, int baudRate) { char portName[16]; sprintf(portName, \\\\.\\COM%d, portNum); HANDLE hCom CreateFileA( portName, GENERIC_READ | GENERIC_WRITE, 0, // 独占模式 NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if (hCom INVALID_HANDLE_VALUE) { DWORD err GetLastError(); // 常见错误2 端口不存在5 端口被占用 printf(Open COM%d failed, error%lu\n, portNum, err); return INVALID_HANDLE_VALUE; } // 配置波特率、数据位、停止位 DCB dcb {0}; dcb.DCBlength sizeof(DCB); GetCommState(hCom, dcb); dcb.BaudRate baudRate; dcb.ByteSize 8; dcb.Parity NOPARITY; dcb.StopBits ONESTOPBIT; dcb.fBinary TRUE; dcb.fDtrControl DTR_CONTROL_ENABLE; dcb.fRtsControl RTS_CONTROL_ENABLE; SetCommState(hCom, dcb); // 设置超时 COMMTIMEOUTS timeouts {0}; timeouts.ReadIntervalTimeout 50; timeouts.ReadTotalTimeoutConstant 2000; timeouts.ReadTotalTimeoutMultiplier 10; timeouts.WriteTotalTimeoutConstant 1000; SetCommTimeouts(hCom, timeouts); return hCom; }三个最常见的失败原因端口号写错COM1 和 COM3 的区别、端口被其他程序占用比如仪器自带的软件没关、DTR/RTS 没使能有些 USB 转串口线需要这两个信号才能工作。3.2 发送指令和读取响应的正确姿势串口读写不是发完就能立刻读到完整响应。GeoCOM 的响应可能分多次到达你需要循环读直到收到\r\n。// 发送指令 bool SendCommand(HANDLE hCom, const char* cmd) { DWORD written; DWORD len strlen(cmd); BOOL ok WriteFile(hCom, cmd, len, written, NULL); return ok written len; } // 读取响应直到遇到 \r\n std::string ReadResponse(HANDLE hCom) { std::string result; char buf[256]; DWORD read; DWORD startTime GetTickCount(); while (GetTickCount() - startTime 3000) { // 3 秒超时 BOOL ok ReadFile(hCom, buf, sizeof(buf) - 1, read, NULL); if (ok read 0) { buf[read] \0; result buf; if (result.find(\r\n) ! std::string::npos) { break; } } Sleep(10); } return result; }ReadIntervalTimeout设 50 毫秒意味着如果两个字节之间间隔超过 50 毫秒读操作就返回。这适合 GeoCOM 这种短指令。如果你设成 0读操作会一直等到缓冲区满结果就是卡死。3.3 一个能跑的最小示例连接、设站、测一个点把上面的函数串起来就是一个最小可跑的程序。流程是打开串口 → 发送COM_GetDeviceInfo确认连接 → 设置测站坐标 → 测量一个点。int main() { HANDLE hCom OpenComPort(3, 19200); if (hCom INVALID_HANDLE_VALUE) return -1; // 1. 确认连接 SendCommand(hCom, %R1Q,5003\r\n); // COM_GetDeviceInfo std::string resp ReadResponse(hCom); if (resp.find(%R1P,0,0) std::string::npos) { printf(Device not responding\n); CloseHandle(hCom); return -1; } printf(Connected: %s\n, resp.c_str()); // 2. 设置测站坐标假设在原点 SendCommand(hCom, %R1Q,2003,0,0,0,0\r\n); // TMC_SetStation resp ReadResponse(hCom); printf(Set station: %s\n, resp.c_str()); // 3. 测量一个点 double hz, v, dist; int ret AutoMeasure(0.0, 90.0, hz, v, dist); // 水平角 0垂直角 90 度 if (ret 0) { printf(Measured: Hz%.4f V%.4f Dist%.4f\n, hz, v, dist); } else { printf(Measure failed, code%d\n, ret); } CloseHandle(hCom); return 0; }TMC_SetStation的参数是东坐标、北坐标、高程、仪器高。如果你不设站测出来的坐标是相对于仪器当前位置的没有实际意义。4. 避坑指南GeoCOM 开发中最容易翻车的五个地方4.1 现象指令发出去没反应串口灯闪了一下原因TPS1200 的 GeoCOM 模式需要手动激活。仪器开机默认是“测量模式”不是“GeoCOM 模式”。你需要在仪器的控制面板里进入“设置 → 接口 → GeoCOM”把它打开。解决如果仪器不在手边可以用COM_GetDeviceInfo测试。如果返回超时先确认仪器屏幕上的接口状态。有些固件版本还需要设置“远程控制”为“允许”。4.2 现象响应里多了一堆乱码解析失败原因串口参数不匹配。TPS1200 默认 19200 波特率但有些 USB 转串口线会自动协商成 9600。另外如果你用的是 RS232 转 USB 线线序不对也会导致数据错乱。解决用串口助手先手动发一条%R1Q,5003\r\n看返回是否正常。如果串口助手正常但程序不正常检查DCB结构体里的fParity和Parity是否一致。4.3 现象测量返回 rc128提示“目标未找到”原因棱镜不在视场内或者自动照准的搜索窗口太小。TPS1200 的AUT_MakePositioning有一个搜索范围参数默认是 ±2 度。如果棱镜偏离超过这个范围就找不到。解决把搜索范围设大一点比如 ±5 度。或者先用AUT_LockIn手动粗略照准再调用AUT_MakePositioning精调。4.4 现象程序跑了一晚上第二天发现数据全是同一个值原因TMC_GetSimpleMea返回的是缓存值。如果你没有在两次测量之间调用TMC_DoMeasure或者改变仪器状态它会一直返回上一次的结果。解决每次测量前先调用TMC_DoMeasure或者用TMC_GetFullMeas代替TMC_GetSimpleMea。后者会强制触发一次新测量。4.5 现象多线程下串口读写冲突程序崩溃原因Windows 串口句柄不是线程安全的。如果你在一个线程里读、另一个线程里写可能会死锁。解决用一个CRITICAL_SECTION或者std::mutex保护串口操作。我一般会把所有串口读写封装到一个单独的类里对外只暴露同步的SendAndReceive方法。class GeoComPort { public: std::string SendAndReceive(const std::string cmd) { std::lock_guardstd::mutex lock(mtx_); SendCommand(hCom_, cmd.c_str()); return ReadResponse(hCom_); } private: HANDLE hCom_; std::mutex mtx_; };5. 从单点测量到自动化监测轮询框架与数据落盘5.1 用状态机管理测量流程单点测量跑通后下一步是自动化。我一般会写一个简单的状态机空闲 → 转动 → 测量 → 记录 → 等待 → 空闲。每个状态有超时超时就重试或跳过。enum State { IDLE, TURNING, MEASURING, RECORDING, WAITING }; struct Point { double targetHz; double targetV; double measuredHz; double measuredV; double dist; int retryCount; }; void MonitorLoop(std::vectorPoint points, int intervalSec) { State state IDLE; size_t index 0; DWORD lastActionTime GetTickCount(); while (true) { switch (state) { case IDLE: if (index points.size()) index 0; state TURNING; lastActionTime GetTickCount(); break; case TURNING: if (GetTickCount() - lastActionTime 5000) { // 转动超时重试 points[index].retryCount; if (points[index].retryCount 3) { index; state IDLE; } else { state TURNING; } lastActionTime GetTickCount(); } else { // 发送转动指令 // ... state MEASURING; } break; case MEASURING: // 触发测量成功则进入 RECORDING // ... break; case RECORDING: // 写入 CSV // ... state WAITING; lastActionTime GetTickCount(); break; case WAITING: if (GetTickCount() - lastActionTime intervalSec * 1000) { index; state IDLE; } break; } Sleep(100); } }这个框架的好处是每个状态只做一件事超时和重试逻辑清晰。你可以根据实际需求调整超时时间比如转动超时设 5 秒测量超时设 3 秒。5.2 数据落盘CSV 格式与断线续传监测数据一般存 CSV方便后续用 Excel 或 Python 分析。格式建议包含时间戳、点号、水平角、垂直角、斜距、东坐标、北坐标、高程、测量状态。void WriteCsv(const std::string filename, const Point p, int pointId) { std::ofstream ofs(filename, std::ios::app); if (!ofs.is_open()) return; // 获取当前时间 SYSTEMTIME st; GetLocalTime(st); char timeBuf[64]; sprintf(timeBuf, %04d-%02d-%02d %02d:%02d:%02d, st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); ofs timeBuf , pointId , p.measuredHz , p.measuredV , p.dist , p.retryCount \n; ofs.close(); }断线续传的思路是每次写入前检查文件是否存在如果存在就追加不存在就创建并写表头。另外程序启动时读取最后一行的时间戳从下一个点继续测。5.3 参数怎么设测量模式与棱镜常数TPS1200 的测量模式有三种标准、快速、跟踪。标准模式精度最高但耗时最长约 2 秒快速模式约 1 秒跟踪模式最快但精度最低适合动态测量。棱镜常数取决于你用的棱镜型号。常见的是 0 和 -30 毫米。如果你用徕卡原装圆棱镜常数通常是 0用第三方棱镜需要查说明书。设错了会导致距离偏差几厘米。参数推荐值说明波特率19200TPS1200 默认值测量模式标准精度优先棱镜常数0 或 -30按棱镜型号转动超时5000 ms角度差大时延长测量超时3000 ms标准模式约 2 秒重试次数3超过则跳过5.4 一个实用的技巧用日志定位通信问题GeoCOM 的调试信息很少出错了只能靠猜。我的习惯是在每次发送和接收时写日志格式是“时间 方向 内容”。这样出问题时能一眼看出是没发出去、还是发了没回、还是回了但解析错了。void LogComm(const std::string dir, const std::string data) { std::ofstream ofs(geocom_log.txt, std::ios::app); SYSTEMTIME st; GetLocalTime(st); char timeBuf[64]; sprintf(timeBuf, %02d:%02d:%02d.%03d, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); ofs timeBuf dir data \n; }日志文件不要太大建议按天分割。我一般会在程序启动时检查日志文件大小超过 10MB 就重命名归档。这套东西我用了好几年从最初的单点测量到后来的 24 小时无人值守监测核心逻辑没变过。GeoCOM 不难难的是细节——波特率、超时、重试、日志每一个都可能让你多花半天。希望帮到你。本文还有配套的精品资源点击获取