CrowdTalkie Goes Offline: LoRa Integration, Command Center, and Twin Cities Rapid Response
When we built CrowdTalkie, we assumed internet connectivity would be available most of the time. Web Bluetooth was our fallback for when networks failed. But real-world deployments taught us that Bluetooth’s 30-meter range creates fragile meshes that break when crowds disperse or when people move between buildings.
The solution: LoRa radio integration. LoRa (Long Range) radios can transmit kilometers with minimal power, forming resilient meshes that work through walls, across neighborhoods, and during complete infrastructure blackouts.
The LoRa Bridge Architecture
Rather than pick a single LoRa protocol, CrowdTalkie implements a unified bridge that supports both major mesh ecosystems:
Meshtastic Integration
Meshtastic is the established player - open-source firmware for common LoRa boards like the LilyGo T-Beam and Heltec V3. It uses Protocol Buffers for message encoding and supports up to 8 encrypted channels per device.
CrowdTalkie connects to Meshtastic devices via Web Bluetooth:
// Connecting to a Meshtastic radio
const device = await connectMeshtastic()
// device: { name: "T-Beam", region: "US (915 MHz)", batteryLevel: 87 }
Messages route through Meshtastic’s PRIVATE_APP portnum, keeping CrowdTalkie traffic separate from regular Meshtastic chat.
MeshCore Integration
MeshCore takes a different approach - purpose-built for high-throughput mesh networking with native binary protocols. It’s newer but offers better performance for real-time coordination.
Both backends implement the same unified interface:
type LoRaBackend = 'meshtastic' | 'meshcore'
interface LoRaDevice {
id: string
name: string
backend: LoRaBackend
firmwareVersion: string
batteryLevel: number
hasGps: boolean
region: string // "United States (915 MHz)", "Europe 868 MHz", etc.
isConnected: boolean
}
The bridge handles backend differences transparently. Send a message, and it encodes appropriately for whichever radio is connected.
Twin Cities Rapid Response Features
The impetus for deep LoRa integration came from community organizers in the Twin Cities requesting features for rapid response scenarios. When a vehicle is spotted multiple times across a demonstration, that information needs to propagate instantly - even when cell networks are overloaded.
Vehicle Sighting Reports
A dedicated vehicle sighting form captures structured data:
- License plate with confidence level (full/partial/none)
- Vehicle type: sedan, SUV, van, truck, unmarked, other
- Color from predefined palette
- Direction of travel: 8-point compass + stationary
- Following status: flag if the reporter is trailing the vehicle
- Photos with automatic processing
interface VehicleSighting {
plate?: string
plateConfidence: 'full' | 'partial' | 'none'
vehicleType?: 'sedan' | 'suv' | 'van' | 'truck' | 'unmarked' | 'other'
vehicleColor?: string
direction?: 'N' | 'NE' | 'E' | 'SE' | 'S' | 'SW' | 'W' | 'NW' | 'stationary'
isFollowing?: boolean
photos?: SightingPhoto[]
location: GeoLocation
}
Photo Capture with OCR
The killer feature: automatic license plate detection. When you snap a photo, CrowdTalkie runs Tesseract.js client-side to extract plate characters.
const result = await readPlateWithPreprocess(imageBlob)
// result: { text: "ABC1234", confidence: 0.87, boundingBox: {...} }
The preprocessing pipeline:
- Scale images to 1200px max for faster processing
- Convert to grayscale
- Boost contrast (1.5x) to separate plate from background
- Run Tesseract with English character recognition
- Post-process to extract plate-like patterns (2-10 alphanumeric chars)
OCR runs entirely in-browser - no server ever sees the image.
Photo Geo-Tagging
Photos often contain GPS coordinates in EXIF metadata. CrowdTalkie extracts them automatically using exifr:
const metadata = await extractPhotoMetadata(imageFile)
// metadata: { location: { lat: 44.9778, lng: -93.2650, accuracy: 5 },
// timestamp: Date, make: "Apple", model: "iPhone 15" }
If the photo lacks GPS data (screenshot, edited image, older camera), the app falls back to the device’s current location with user permission.
LoRa Message Protocol for Vehicles
Vehicle sightings need to traverse the LoRa mesh efficiently. We defined compact binary message types:
type LoRaMessageType =
| 'text' // Regular chat
| 'poi' // Point of interest
| 'vehicle' // Vehicle sighting (0x10)
| 'plate_lookup' // Cross-zone plate query (0x11)
| 'plate_response' // Query response (0x12)
| 'handoff_req' // Zone handoff request (0x13)
| 'handoff_ack' // Handoff acknowledgment (0x14)
Vehicle sightings encode to a compact format for LoRa’s limited bandwidth:
interface LoRaVehicleSighting {
p?: string // Plate
v?: number // Vehicle type (0-5 enum)
c?: number // Color index
d?: number // Direction (0-8 enum)
f?: boolean // Is following
la: number // Latitude (6 decimals)
lo: number // Longitude (6 decimals)
de?: string // Description (max 50 chars)
}
Zone Handoff Protocol
Large demonstrations span multiple neighborhoods. Each area may have its own CrowdTalkie room. When a vehicle leaves one zone and enters another, the zone handoff protocol ensures continuity:
- Follower in Zone A spots vehicle heading east toward Zone B
- Sends
handoff_reqmessage with vehicle details and boundary street - Zone B room receives the request, displays alert with vehicle info
- Zone B coordinator sends
handoff_ackto confirm receipt - Zone A follower sees confirmation - handoff complete
interface LoRaHandoffRequest {
r: string // Request ID
v: LoRaVehicleSighting // Vehicle details
z: string // Source zone name
st?: string // Street name at boundary
}
This works over LoRa or regular P2P, whichever path is available.
Multi-Language Support
The Twin Cities community requested Spanish language support. CrowdTalkie now includes full i18n using react-i18next:
- English and Spanish translations for all UI elements
- Vehicle types, colors, and directions translated
- Accessibility labels translated
- Language selector in settings
The interface respects the user’s browser language preference but allows manual override.
Alert Sounds
When high-priority messages arrive - vehicle sightings, blocked routes, emergency reports - an audio alert plays. This uses the Web Audio API with user-configurable volume and the ability to disable entirely:
// Priority messages trigger alert
if (message.type === 'vehicle_sighting' || message.priority === 'high') {
playAlertSound()
}
The sound is a brief synthesized tone, not a jarring alarm. It gets attention without causing panic.
The Command Center: Incident Command for Crowds
The most significant architectural addition is the Command Center - a hierarchical coordination layer inspired by the Incident Command System (ICS) used by emergency responders.
How It Works
Large actions often span multiple neighborhoods, each with their own coordination needs. The Command Center lets organizers create a parent “command room” that oversees multiple “sector rooms”:
Command Room (Citywide Coordination)
├── Sector: Uptown
├── Sector: Powderhorn
├── Sector: Longfellow
└── Sector: Downtown
The commander sees a unified dashboard with:
- Map visualization of all sectors with boundaries
- Participant counts per sector
- Aggregated POI reports from all zones
- Pending escalations requiring attention
Remote Commands
Commanders can issue authenticated remote commands to any sector:
type RemoteCommandPayload =
| { type: 'pause_room' } // Freeze comms temporarily
| { type: 'resume_room' } // Resume operations
| { type: 'kick_participant'; targetPeerId: string }
| { type: 'update_mode'; mode: 'mic' | 'chat'; value: string }
| { type: 'update_boundary'; centerLat; centerLng; radiusMeters }
| { type: 'set_alert_status'; status: 'active' | 'alert' | 'moving' }
| { type: 'clear_all_pois' }
Every command is cryptographically signed with Ed25519. Sector rooms verify the commander’s public key before executing. No impersonation possible.
Escalation Protocol
When a sector coordinator spots something that needs citywide attention, they can escalate:
- Coordinator tags a message/POI with priority level (routine/urgent/critical)
- Escalation appears in Command Center dashboard
- Commander acknowledges or broadcasts response to all sectors
- Sector receives feedback that the escalation was seen
This mirrors how ICS handles information flow: ground teams report upward, commanders broadcast directives downward.
Broadcast Messages
The Command Center can send broadcasts to all sectors or targeted subsets:
- Priority levels: Routine (green), Urgent (yellow), Critical (red)
- Target selection: All sectors, or pick specific zones
- Audio alerts: Critical broadcasts trigger sound notifications in receiving sectors
Deputy Sharing
Commanders can share their Command Center with deputies:
- Generate a “deputy link” that grants command access
- Deputies see the same dashboard and can issue commands
- All actions are signed with the deputy’s key (attribution preserved)
- Link can be revoked by generating a new one
Sector Management
From the Command Center, organizers can:
- Create new sectors with name, location, and radius
- Adjust sector boundaries as the situation evolves
- Assign sector leads from active participants
- Monitor sector health via participant count and activity
The system suggests boundary adjustments when participant density shifts - if most people in a sector cluster near one edge, it may recommend expanding that direction.
Why This Matters
Traditional protest coordination often breaks down at scale. With 50 people, shouting works. With 5,000 people spread across a city, you need structure.
The Command Center provides that structure without sacrificing CrowdTalkie’s decentralized privacy model:
- No central server - command/sector relationships are P2P
- Cryptographic authority - only verified commanders can issue orders
- Resilient hierarchy - if command goes offline, sectors continue independently
- LoRa compatibility - commands traverse the LoRa mesh when internet fails
It’s Incident Command without the bureaucracy.
What This Enables
With these features, CrowdTalkie supports scenarios that were previously impossible:
Complete infrastructure failure: LoRa radios form a mesh covering square kilometers. Even if cell towers go down and WiFi dies, coordinated communication continues.
Vehicle tracking across zones: A suspicious vehicle reported in Uptown appears on dashboards in Powderhorn and Longfellow before it arrives. Each zone knows what to look for.
Photo evidence with provenance: Every photo has GPS coordinates (EXIF or device), timestamp, and cryptographic signature from the sender. The chain of custody is clear.
Bandwidth-constrained operation: LoRa moves kilobits per second. The compact binary protocol ensures vehicle alerts traverse the mesh in seconds, not minutes.
Hardware Requirements
To use LoRa features, you need a compatible radio:
Meshtastic-compatible devices:
- LilyGo T-Beam (recommended - built-in GPS)
- Heltec LoRa 32 V3
- RAK WisBlock
MeshCore-compatible devices:
- MeshCore boards with BLE support
Devices cost $25-60. One radio per room is sufficient - it bridges the room’s P2P mesh to the LoRa mesh.
Privacy Preserved
The LoRa integration maintains CrowdTalkie’s core privacy guarantees:
- End-to-end encryption: Messages encrypt before leaving the device with room-derived AES-256-GCM keys
- Per-room identity: Your LoRa peerId in Room A differs from Room B
- No persistent identity: When the session ends, the identity vanishes
- Local processing: OCR and geo-tagging happen client-side; no cloud services
LoRa relay nodes (other Meshtastic/MeshCore devices in range) see only encrypted blobs.
Try It
CrowdTalkie with LoRa integration is live at crowdtalkie.com. The Field Manual documents the new features.
If you’re organizing in an area with infrastructure concerns, consider deploying a few LoRa radios. They’re inexpensive, solar-chargeable, and extend your coordination range from 30 meters (Bluetooth) to several kilometers.
The mesh just got a lot more resilient.
CrowdTalkie is open source and part of the Antifascist Fun Brigade toolkit. Contributions welcome.