๐ Taipei Bus
Fixed-Point Vehicle OD Skill
Integrates Taipei City fixed-point vehicle OD real-time API with PTX open data. Provides real-time bus position tracking, stop-sequence-based ETA estimation, route search, stop sequence lookup, anomaly detection, hotspot analysis, and map visualization.
๐ What Can This Skill Do?
taipei-bus is a bus dynamic query skill built for OpenClaw Agents.
It integrates three core data sources, focused on solving the most common question:
"When will the bus arrive?"
Each record: plate, route, stop, direction
28,741 stops, locally cached
415 routes, outbound & return sequences
TstBusEvent RouteID (e.g., 158701) and
PTX RouteID (e.g., 10132, corresponding to Route 234) are
completely independent systems. They do not share IDs.
Use stop name or route name search to establish cross-references.
๐ฆ How to Install
Two installation methods are supported. Method 1 (SkillHub) is recommended and handles all dependencies automatically.
Method 1: SkillHub Install (Recommended)
skillhub_install install_skill taipei-bus
Method 2: Manual Copy
Manually copy the lib/ and data/ directories to your project:
# Copy lib/ (bus-api.js + route-api.js + stop-coords.js)
cp -r ~/.qclaw/skills/taipei-bus/lib ./taipei-bus-lib
# Copy cached data (28,741 stop coords + 415 route sequences)
cp -r ~/.qclaw/skills/taipei-bus/data ./taipei-bus-data
# Or copy the entire skill
cp -r ~/.qclaw/skills/taipei-bus /your/project/
fetch API,
which requires Node.js 18 or higher. For older versions, install node-fetch as a polyfill.
โก Your First Bus Query
Prerequisites
- Node.js 18.0+ (with native
fetch) - Skill installed with
lib/anddata/directories copied - Network access (to PTX API + TCG Bus API)
Basic Query: When will Route 234 arrive?
const { searchRouteByName, etaBySequence, getRouteStopSequence } = require('./lib/bus-api.js');
async function main() {
// Step 1: Search by route name (numbers, trunk names, colors all work)
const route = searchRouteByName('234');
if (!route) {
console.log('Route not found');
return;
}
console.log(`Found: ${route.routeName} (RouteID: ${route.routeId})`);
// Step 2: Get outbound stop sequence
const stops = getRouteStopSequence(route.routeId, 0); // 0=outbound, 1=return
console.log(`Total stops: ${stops.length}`);
// Step 3: Find target stop (by partial name match)
const target = stops.find(s => s.stopName.includes('Huwanzayuan'));
if (!target) {
console.log('Target stop not found in sequence');
return;
}
// Step 4: Estimate arrival by stop sequence (most accurate)
const eta = etaBySequence(route.routeId, target.stopId, 0);
console.log(`\n๐ ${route.routeName} โ ${target.stopName}`);
console.log(`โฑ๏ธ ETA: ~${eta.minutes} minutes`);
console.log(`๐ ${eta.note}`);
}
main().catch(console.error);
Run It
node quickstart.js
# Output:
# Found: 234 (RouteID: 10132)
# Total stops: 40
# ๐ 234 โ Huwanzayuan
# โฑ๏ธ ETA: ~8 minutes
# ๐ By sequence (stop 12/40), 5 stops to target (outbound)
๐ AI Agent Trigger Keywords
Mentioning these keywords in an OpenClaw Agent chat activates this skill
| Category | Keywords | Description |
|---|---|---|
| Dynamic | ๅ
ฌ่ปๅๆ
/bus dynamic, ๅ
ฌ่ปๅคไน
/bus ETA, ็ญๅ
ฌ่ป, ๅ
ฌ่ปๅฐ็ซ |
Query bus arrival time |
| Tracking | ๅ
ฌ่ป่ฟฝ่นค, ๅ
ฌ่ปไพไบ, ๅ
ฌ่ป่ฟฝ่นค |
Track a specific bus |
| Map | ๅ
ฌ่ปๅฐๅ, ๅ
ฌ่ป็ซ็, ๅ
ฌ่ป็ฑ้ป, bus map |
Map visualization or stop lookup |
| Route | ๅ
ฌ่ป่ทฏ็ท, ๅ
ฌ่ป้ฆ็ญ, ๅ
ฌ่ปๆซ็ญ, bus route |
Route info or search |
| Anomaly | ๅ
ฌ่ป่ซ็ญ, ๅ
ฌ่ป็ฐๅธธ, bus anomaly |
Anomaly or skip detection |
| Mixed | Taipei bus, ๅฐๅๅ
ฌ่ป, ่บๅๅธๅ
ฌ่ป |
Mixed-language triggers |
๐ก Four API Data Sources
| Source | URL / Endpoint | Contents | Cache Strategy |
|---|---|---|---|
| Live TstBusEvent |
tcgbusfs.blob.core.windows.net/blobbus/TstBusEvent.json |
Plate, RouteID, StopID, direction, DataTime, DutyStatus, BusStatus | None (live fetch each time) |
| PTX Stop Stop coords |
ptx.transportdata.tw/MOTC/v2/Bus/Stop/City/Taipei |
28,741 stops โ name, lat/lon, StationID | data/stop-coords.json (~2 MB, quarterly rebuild) |
| PTX Route Route list |
ptx.transportdata.tw/MOTC/v2/Bus/Route/City/Taipei |
415 routes โ name, departure/terminal, first/last bus, operator | data/route-full-cache.json (~11 MB, quarterly rebuild) |
| PTX StopOfRoute Stop sequences |
ptx.transportdata.tw/MOTC/v2/Bus/StopOfRoute/City/Taipei |
Outbound/return stop sequences per route with StopIDs and coordinates | data/route-full-cache.json (~11 MB, quarterly rebuild) |
scripts/build-cache.py.
To update PTX data, run:
python3 ~/.qclaw/skills/taipei-bus/scripts/build-cache.py
โ๏ธ Complete Function Reference
All functions are imported via require('./lib/bus-api.js')
๐ก Real-time Vehicle Functions (bus-api.js)
Promise<BusRecord[]> โ sorted by DataTime descending
const { fetchAll } = require('./lib/bus-api.js');
const buses = await fetchAll();
console.log(`Total buses online: ${buses.length}`);
buses.slice(0, 3).forEach(b => console.log(b.BusID, b.RouteID, b.StopID));
Promise<BusRecord[]>
| Param | Type | Description |
|---|---|---|
routeId | string | TstBusEvent RouteID (e.g., '158701') โ ๏ธ Not a PTX RouteID |
routeId uses the TstBusEvent system (e.g., 158701),
NOT the PTX RouteID (e.g., 10132). Use searchRouteByName to confirm first.
const { findByRoute } = require('./lib/bus-api.js');
const buses = await findByRoute('158701');
console.log(`Route: ${buses.length} buses online`);
buses.forEach(b => console.log(`${b.BusID} at ${b.StopID}`));
Promise<BusRecord | null>
const { findByBus, formatBus } = require('./lib/bus-api.js');
const bus = await findByBus('KKC-1100');
if (bus) {
console.log(formatBus(bus, true));
} else {
console.log('Bus not found or offline');
}
Promise<BusRecord[]>
const { findAtStation } = require('./lib/bus-api.js');
const buses = await findAtStation('179647');
console.log(`${buses.length} buses at stop`);
buses.forEach(b => console.log(b.BusID, b.RouteID));
Promise<BusRecord[]> โ empty = all normal
const { detectAnomalies, formatBus } = require('./lib/bus-api.js');
const anomalies = await detectAnomalies();
if (anomalies.length === 0) {
console.log('โ
All systems normal');
} else {
console.log(`โ ๏ธ ${anomalies.length} anomalies`);
anomalies.forEach(b => console.log(formatBus(b)));
}
Promise<{[routeId]: {count, buses[]}}>
const { routeSummary } = require('./lib/bus-api.js');
const summary = await routeSummary();
const top5 = Object.entries(summary)
.sort(([,a],[,b]) => b.count - a.count)
.slice(0, 5);
top5.forEach(([routeId, {count}]) => {
console.log(`Route ${routeId}: ${count} buses`);
});
๐บ๏ธ PTX Coordinate Functions (bus-api.js + stop-coords.js)
Promise<EnrichedBusRecord[]> โ each record has _stopCoords field
const { fetchAllWithCoords } = require('./lib/bus-api.js');
const buses = await fetchAllWithCoords();
const first = buses[0];
console.log(first.BusID, first.RouteID);
console.log('๐', first._stopCoords?.name,
`(${first._stopCoords?.lat?.toFixed(5)}, ${first._stopCoords?.lon?.toFixed(5)})`);
Promise<Array<{stopId, stopName, lat, lon, count}>>
const { getHotspots } = require('./lib/bus-api.js');
const hotspots = await getHotspots(5);
hotspots.forEach((h, i) => {
console.log(`${i+1}. ${h.stopName} โ ${h.count} buses`);
console.log(` ๐ (${h.lat.toFixed(5)}, ${h.lon.toFixed(5)})`);
});
{name, lat, lon, stationId} | null
const { getStopCoords } = require('./lib/bus-api.js');
const coords = getStopCoords('33210');
if (coords) {
console.log(coords.name); // "Gongguan"
console.log(coords.lat, coords.lon);
}
Array<{stopId, name, lat, lon...}>
const { searchStops } = require('./lib/bus-api.js');
const hits = searchStops('MRT', 5);
hits.forEach(s => {
console.log(`${s.stopId}: ${s.name}`);
console.log(` ๐ (${s.lat?.toFixed(5)}, ${s.lon?.toFixed(5)})`);
});
๐ PTX Route Functions (bus-api.js + route-api.js)
RouteInfo | null โ includes routeId, routeName, departure, terminal
const { searchRouteByName } = require('./lib/bus-api.js');
const route = searchRouteByName('Blue10');
// also works: '่10', '10', etc.
if (route) {
console.log(`${route.routeName} (${route.routeId})`);
console.log(`${route.departure} โ ${route.terminal}`);
}
RouteInfo | null
const { getRouteInfo, routeApi } = require('./lib/bus-api.js');
const route = getRouteInfo('10132'); // Route 234
console.log(routeApi.formatRoute(route));
RouteInfo[]
const { searchRoutes } = require('./lib/bus-api.js');
const routes = searchRoutes('Blue', 5);
routes.forEach(r => console.log(`${r.routeName}: ${r.departure} โ ${r.terminal}`));
StopEntry[] โ includes stopId, stopName, lat, lon
| Param | Type | Default | Description |
|---|---|---|---|
routeId | string | โ | PTX RouteID |
direction | number | 0 | 0=outbound, 1=return |
const { getRouteStopSequence } = require('./lib/bus-api.js');
const stops = getRouteStopSequence('10132', 0); // Route 234 outbound
stops.forEach((s, i) => {
console.log(`${i+1}. ${s.stopName} (${s.stopId})`);
});
{minutes, remaining, total, stop, note}
| Param | Type | Default | Description |
|---|---|---|---|
routeId | string | โ | PTX RouteID |
targetStopId | string | โ | Target PTX StopID |
direction | number | 0 | 0=outbound, 1=return |
avgSecPerStop | number | 90 | Avg seconds per stop (default: 1.5min urban) |
const { etaBySequence } = require('./lib/bus-api.js');
const eta = etaBySequence('10132', '33210', 0);
console.log(`ETA: ${eta.minutes} minutes (${eta.note})`);
console.log(`${eta.remaining}/${eta.total} stops remaining`);
{routeId, routeName, direction, index}[]
const { getRoutesByStop } = require('./lib/bus-api.js');
const routes = getRoutesByStop('33210');
routes.forEach(r => {
const dir = r.direction === 0 ? 'Outbound' : 'Return';
console.log(`${r.routeName} (${dir}) stop #${r.index}`);
});
โฑ๏ธ Three-Tier ETA Accuracy System
| Tier | Method | Accuracy | When to Use |
|---|---|---|---|
| โ
โ
โ
โ
โ
Sequence |
remaining stops ร 90s | ยฑ1-2 stops | Known bus position + complete sequence (etaBySequence) |
| โ
โ
โ
Haversine |
straight-line distance รท 25 km/h | ยฑ2-5 min | Has bus coords + stop coords (etaEstimate) |
| โ
Fallback |
500m รท 25 km/h = 1.2 min | Coarse estimate | No coordinates available (fixed stop distance) |
๐ก Five Real-World Scenarios
const { searchRouteByName, etaBySequence, getRouteStopSequence } = require('./lib/bus-api.js');
async function getBusEta(routeName, targetStopName) {
const route = searchRouteByName(routeName);
if (!route) return `Route "${routeName}" not found`;
const stops = getRouteStopSequence(route.routeId, 0);
const target = stops.find(s => s.stopName.includes(targetStopName));
if (!target) return `Stop "${targetStopName}" not in sequence`;
const eta = etaBySequence(route.routeId, target.stopId, 0);
return `๐ ${route.routeName} โ ${target.stopName}: ~${eta.minutes} min\n${eta.note}`;
}
await getBusEta('234', 'Huwanzayuan');
const { detectAnomalies, formatBus } = require('./lib/bus-api.js');
async function checkSystemHealth() {
const anomalies = await detectAnomalies();
if (anomalies.length === 0) return 'โ
All buses operating normally';
const lines = [`โ ๏ธ ${anomalies.length} anomalies found:\n`];
anomalies.forEach(b => {
const reasons = [];
if (b.DutyStatus !== '1') reasons.push(`Duty(${b.DutyStatus})`);
if (b.BusStatus !== '0') reasons.push(`Status(${b.BusStatus})`);
lines.push(`๐จ ${b.BusID} Route:${b.RouteID} โ ${reasons.join(', ')}`);
});
return lines.join('\n');
}
await checkSystemHealth();
const { searchRouteByName, getRouteStopSequence } = require('./lib/bus-api.js');
function showRouteStops(routeName, direction = 0) {
const route = searchRouteByName(routeName);
if (!route) return;
const dirLabel = direction === 0 ? 'Outbound' : 'Return';
const stops = getRouteStopSequence(route.routeId, direction);
console.log(`๐ ${route.routeName} ${dirLabel} (${stops.length} stops)\n`);
stops.forEach((s, i) => console.log(` ${String(i+1).padStart(2,'0')}. ${s.stopName}`));
}
showRouteStops('Blue10', 0);
const { searchStops, getRoutesByStop } = require('./lib/bus-api.js');
async function findBusRoutesAtStop(stopKeyword) {
const hits = searchStops(stopKeyword, 3);
if (!hits.length) return `No stops matching "${stopKeyword}"`;
const results = [];
for (const stop of hits) {
const routes = getRoutesByStop(stop.stopId);
results.push({
stop: stop.name,
routes: routes.map(r => `${r.routeName}(${r.direction===0?'Out':'Ret'}#${r.index})`)
});
}
return results;
}
await findBusRoutesAtStop('MRT Gongguan');
// Use qclaw-cron-skill to check every 5 minutes
cron(action='add', job={
name: 'Taipei Bus Anomaly Detection',
schedule: { kind: 'every', everyMs: 5 * 60 * 1000 },
sessionTarget: 'isolated',
payload: {
kind: 'agentTurn',
message: `Run detectAnomalies() and report. If anomalies found,
output "โ ๏ธ N anomalies: plate/route/reason"; otherwise "โ
All normal".`,
timeoutSeconds: 60
},
delivery: { mode: 'announce' }
})
See examples/example-cron.js for more templates:
every-5-min anomaly detection, daily morning route summary,
and one-shot ETA reminder.
๐ฌ Conversational Use Case Reference
User query โ Agent function โ Response format
searchRouteByName + etaBySequenceโ ~N minutes (with sequence details)
searchStops + findAtStationโ N buses at stop (plate list)
findByBus + formatBusโ plate/route/stop/direction/DataTime
getHotspotsโ TOP N stops (name + count + coords)
searchRouteByName + getRouteStopSequenceโ Full stop list (outbound/return)
detectAnomaliesโ โ All normal or โ ๏ธ N anomalies (with details)
searchRouteByName + getRouteInfoโ First/last bus times outbound & return
"I'm at XX stop, going to OO address" requires:
โ Address geocoding โ โก Find nearest stop โ โข Transfer planning.
Geocoding and transfer algorithms are not yet implemented.
Consider pairing with tencentmap-jsapi-gl-skill (Tencent Maps geocoding API)
or Google Maps / TPTP public transit API for full routing.
โ ๏ธ Known Limitations โ Read Before Using
TstBusEvent RouteID/StopID (e.g., 158701, 179647)
and PTX RouteID/StopID (e.g., 10132, 33210)
are completely independent. Cross-references must be built via
stop/route name search. Use searchRouteByName to get PTX RouteID first.
etaBySequence) is limited by:
โ The TstBusEvent only reports the last reported stop, not real-time GPS tracks;
โก The cache is a static snapshot; actual sequences may vary slightly with road conditions.
Always label results as "estimated" rather than "exact".
tencentmap-jsapi-gl-skill to geocode addresses and find
nearby stops, but complex transfers should be delegated to Google Maps / TPTP.
data/) are generated by
scripts/build-cache.py, default quarterly.
If PTX data is updated (route changes, stop relocations), run the rebuild script manually.
๐ Deploy to GitHub Pages
This documentation site is pure static HTML โ deployable to GitHub Pages, Netlify, Vercel, or any static host. GitHub Pages example below.
docs/ contains all HTML, CSS, and JS files,
then push to your GitHub repository (use a gh-pages branch or main).
main branch, folder to /docs.
https://<username>.github.io/<repo>/.
taipei-bus.openclaw.ai) in docs/CNAME,
then add a CNAME DNS record pointing to <username>.github.io.
๐ Licensing & Attribution
Bus dynamic data collected and integrated by this skill is provided under OGDL. Free for commercial and non-commercial use with attribution to government open data sources.
PTX Open Data Platform data is provided under its own terms. This skill's PTX integration modules are licensed under MIT.