Skip to content

Commit 8e82cb0

Browse files
committed
Support HTTPS Influx client; add wide-schema docs, v0.2.1
Introduce an HTTP-based Influx client and wide-format (columnar) workflow across the library and docs. Changes include: - Implement HttpInfluxClient plus _ArrowLike/_Scalar adapters in src/slicks/fetcher.py and auto-select it in get_influx_client() for https:// hosts; preserve InfluxDBClient3 for non-HTTPS. This enables querying via /api/v3/query_sql when gRPC/Flight is not available. - Update discover_sensors to use get_influx_client() and relax client typing/creation, and simplify narrow/wide paths. - Expand fetcher typing and client selection logic; add minimal adapters so HTTP responses can be consumed like Arrow results. - API/docs updates: emphasize wide vs narrow schemas, add schema and resample params to fetch_telemetry, document fetch_telemetry_chunked, WideWriter, CAN decoding and line-protocol writing, and update examples to use schema="wide". - Bump package version to 0.2.1 and add httpx dependency to pyproject.toml. - Misc: update README examples (default DB and wide schema example) and add CLAUDE.md to .gitignore. These changes make the library more robust over HTTPS tunnels and standardize wide-format telemetry usage and writing.
1 parent 22b72c2 commit 8e82cb0

9 files changed

Lines changed: 289 additions & 64 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ coverage.xml
1616
htmlcov/
1717
/examples/__pycache__
1818
.DS_Store
19+
CLAUDE.md

README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
The home baked data pipeline for **Western Formula Racing**.
88

99
This package handles:
10-
1. **Data Ingestion:** Reliable fetching from InfluxDB 3.0.
11-
2. **Movement Detection:** Smart filtering of "Moving" vs "Idle" car states.
12-
3. **Sensor Discovery:** Tools to explore available sensors on any given race day.
10+
1. **Data Ingestion:** Reliable fetching from InfluxDB 3.0 in wide (columnar) or narrow (legacy EAV) format.
11+
2. **Data Writing:** `WideWriter` encodes CAN frames directly to InfluxDB wide format line protocol.
12+
3. **Movement Detection:** Smart filtering of "Moving" vs "Idle" car states.
13+
4. **Sensor Discovery:** Tools to explore available sensors on any given race day.
1314

1415
## Documentation
1516

@@ -32,14 +33,15 @@ pip install slicks
3233
import slicks
3334
from datetime import datetime
3435

35-
# 1. Connect (Auto-configured or custom)
36-
slicks.connect_influxdb3(db="WFR25")
36+
# 1. Connect (auto-configured from env vars or explicit)
37+
slicks.connect_influxdb3(db="WFR26")
3738

38-
# 2. Fetch Data (One-liner)
39+
# 2. Fetch Data — wide format (columnar, preferred)
3940
df = slicks.fetch_telemetry(
40-
datetime(2025, 9, 28),
41-
datetime(2025, 9, 30),
42-
"INV_Motor_Speed"
41+
datetime(2025, 9, 28),
42+
datetime(2025, 9, 30),
43+
"INV_Motor_Speed",
44+
schema="wide",
4345
)
4446

4547
print(df.describe())

docs/advanced_usage.md

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,42 @@
11
# Advanced Usage & Workflows
22

3-
## 1. Dynamic Sensor Discovery
4-
Not sure what sensors are available for a specific test day? Don't guess. Use the discovery tool.
3+
## 1. Wide vs Narrow Format
4+
5+
The database stores telemetry in **wide format**: each CAN signal is its own column. This is faster to query and requires no pivot step.
56

67
```python
7-
from slicks import discover_sensors
8+
import slicks
89
from datetime import datetime
910

10-
start = datetime(2025, 9, 28)
11-
end = datetime(2025, 9, 30)
11+
start = datetime(2025, 9, 28, 12, 0)
12+
end = datetime(2025, 9, 28, 14, 0)
13+
14+
# Wide format (default, preferred) — direct column access
15+
df = slicks.fetch_telemetry(start, end, ["INV_Motor_Speed", "PackCurrent"], schema="wide")
16+
17+
# Narrow format (legacy EAV) — only for old data that was never migrated
18+
df = slicks.fetch_telemetry(start, end, ["INV_Motor_Speed", "PackCurrent"], schema="narrow")
19+
```
20+
21+
Use `schema="wide"` for all new work.
22+
23+
---
24+
25+
## 2. Dynamic Sensor Discovery
26+
Not sure what sensors are available? With wide format, discovery is instant — it reads column metadata rather than scanning data rows.
27+
28+
```python
29+
from slicks import discover_sensors
1230

13-
# This physically queries the DB to find what tags exist
14-
available_sensors = discover_sensors(start, end)
31+
# Wide: instant metadata lookup (no time range needed)
32+
available_sensors = discover_sensors(None, None, schema="wide")
1533

1634
print(f"Found {len(available_sensors)} sensors:")
1735
for sensor in available_sensors:
1836
print(f" - {sensor}")
1937
```
2038

21-
## 2. Managing Environments
39+
## 3. Managing Environments
2240
You often need to switch between `Development`, `Testing`, and `Production` databases, or switch to a local replay server.
2341

2442
### Option A: Environment Variables (Best for CI/CD)
@@ -38,7 +56,7 @@ slicks.connect_influxdb3(
3856
)
3957
```
4058

41-
## 3. Bulk Export for CSV Analysis
59+
## 4. Bulk Export for CSV Analysis
4260
If you need to hand off data to the aerodynamics team who uses Excel/MATLAB, use the bulk fetcher. It handles day-by-day chunking to avoid crashing the computer.
4361

4462
```python
@@ -48,14 +66,41 @@ from slicks import bulk_fetch_season
4866
bulk_fetch_season(start, end, output_file="full_weekend_data.csv")
4967
```
5068

51-
## 4. Customizing Movement Detection
69+
## 5. Writing CAN Data (Wide Format)
70+
71+
If you're ingesting raw CAN bus data (e.g., from a replay script or live logger), use `WideWriter`. It decodes CAN frames using a DBC file and writes them as wide format line protocol.
72+
73+
```python
74+
from slicks import WideWriter
75+
76+
writer = WideWriter(
77+
url="http://localhost:8086",
78+
token="my-token",
79+
bucket="WFR26",
80+
measurement="WFR26",
81+
dbc_path="path/to/WFR26.dbc",
82+
)
83+
84+
# Decode and queue a CAN frame
85+
writer.decode_and_queue(can_id=0x200, data=bytes([0x01, 0x02, ...]), ts_ns=timestamp_ns)
86+
87+
# Flush remaining data when done
88+
writer.close()
89+
```
90+
91+
Each decoded CAN message becomes one row with all of its signals as fields:
92+
```
93+
WFR26,messageName=BMS_Status,canId=512 PackCurrent=-3264.0,SOC=85.0 1700000000000000000
94+
```
95+
96+
## 6. Customizing Movement Detection
5297
If you are analyzing **Charging** or **Static Testing**, the default movement filter will hide your data. Disable it:
5398

5499
```python
55100
# Fetch Battery Current even when car is stopped
56101
df = slicks.fetch_telemetry(
57-
start, end,
58-
signals="PackCurrent",
102+
start, end,
103+
signals="PackCurrent",
59104
filter_movement=False
60105
)
61106
```

docs/api_reference.md

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,55 @@ slicks.connect_influxdb3(url=None, token=None, org=None, db=None)
2020

2121
### `slicks.fetch_telemetry`
2222

23-
The primary function to retrieve data. It handles querying, pivoting, resampling, and movement filtering.
23+
The primary function to retrieve data. It handles querying, resampling, and movement filtering.
2424

2525
```python
26-
slicks.fetch_telemetry(start_time, end_time, signals=None, client=None, filter_movement=True)
26+
slicks.fetch_telemetry(start_time, end_time, signals=None, client=None,
27+
filter_movement=True, resample="1s", schema="wide")
2728
```
2829

2930
- **start_time** *(datetime)*: Start of the query range.
3031
- **end_time** *(datetime)*: End of the query range.
31-
- **signals** *(str or list[str])*: A single sensor name or a list of sensor names to fetch. Defaults to standard configuration if None.
32+
- **signals** *(str or list[str])*: A single sensor name or a list of sensor names to fetch. Defaults to standard configuration if `None`.
3233
- **client** *(InfluxDBClient3, optional)*: An existing client instance (advanced use).
33-
- **filter_movement** *(bool)*: If `True` (default), strips out rows where the car is stationary. If `False`, returns all raw data.
34+
- **filter_movement** *(bool)*: If `True` (default), strips out rows where the car is stationary.
35+
- **resample** *(str or None)*: Pandas frequency string for resampling (e.g. `"1s"`, `"100ms"`). Pass `None` for raw data.
36+
- **schema** *(str)*: `"wide"` (default, columnar — each signal is a column) or `"narrow"` (legacy EAV — requires pivot).
3437

35-
**Returns:** `pandas.DataFrame` indexed by time, with 1-second resolution. Returns `None` if no data is found.
38+
**Returns:** `pandas.DataFrame` indexed by time. Returns `None` if no data is found.
39+
40+
---
41+
42+
### `slicks.fetch_telemetry_chunked`
43+
44+
Same interface as `fetch_telemetry`, but splits large date ranges into chunks and runs them in parallel. Handles server resource limits automatically via adaptive query bisection.
45+
46+
```python
47+
slicks.fetch_telemetry_chunked(start_time, end_time, signals=None, client=None,
48+
filter_movement=True, resample="1s", schema="wide",
49+
chunk_size=timedelta(hours=6), max_workers=4)
50+
```
51+
52+
- **chunk_size** *(timedelta)*: Window size per chunk (default: 6 hours).
53+
- **max_workers** *(int)*: Number of parallel threads (default: 4).
54+
55+
**Returns:** `pandas.DataFrame` concatenated from all chunks, or `None`.
3656

3757
---
3858

3959
### `slicks.discover_sensors`
4060

41-
Scans the database to find which sensors actually recorded data during a time period.
61+
Returns the list of available sensor/signal names.
4262

4363
```python
44-
slicks.discover_sensors(start_time, end_time, chunk_size_days=1)
64+
slicks.discover_sensors(start_time, end_time, chunk_size_days=7,
65+
client=None, show_progress=True, schema="wide")
4566
```
4667

47-
- **start_time** *(datetime)*: Start of scan.
48-
- **end_time** *(datetime)*: End of scan.
49-
- **chunk_size_days** *(int)*: How many days to query at once (prevents timeouts).
68+
- **start_time** *(datetime)*: Start of scan (used only in `"narrow"` schema).
69+
- **end_time** *(datetime)*: End of scan (used only in `"narrow"` schema).
70+
- **chunk_size_days** *(int)*: Days per chunk for narrow schema scans (default: 7).
71+
- **schema** *(str)*: `"wide"` performs an instant metadata lookup (`information_schema.columns`) — no time range required. `"narrow"` scans actual data rows.
5072

5173
**Returns:** `list[str]` of unique sensor names sorted alphabetically.
5274

@@ -76,3 +98,66 @@ slicks.detect_movement_ratio(df, speed_column="INV_Motor_Speed")
7698
```
7799

78100
**Returns:** `dict` containing `total_rows`, `moving_rows`, `idle_rows`, and `movement_ratio` (0.0 - 1.0).
101+
102+
---
103+
104+
## Wide Format Writing
105+
106+
### `slicks.WideWriter`
107+
108+
Encodes CAN frames to InfluxDB wide format line protocol and writes them in batches.
109+
110+
```python
111+
from slicks import WideWriter
112+
113+
writer = WideWriter(
114+
url, # InfluxDB URL
115+
token, # Auth token
116+
bucket, # Bucket/database name (e.g. "WFR26")
117+
measurement, # Measurement name (e.g. "WFR26")
118+
dbc_path=None, # Path to DBC file (or set WFR_DBC_PATH env var)
119+
batch_size=5000, # Points per write batch
120+
)
121+
```
122+
123+
**Methods:**
124+
125+
- `decode_and_queue(can_id, data, ts_ns)` — Decode raw CAN bytes and queue for batch write.
126+
- `write_lines(lines)` — Write pre-formatted line protocol strings directly.
127+
- `flush()` — Flush the pending batch.
128+
- `close()` — Flush and close the connection.
129+
130+
**Line protocol format:**
131+
```
132+
WFR26,messageName=BMS_Status,canId=512 PackCurrent=-3264.0,SOC=85.0 1700000000000000000
133+
```
134+
135+
---
136+
137+
## CAN Decoding
138+
139+
### `slicks.decode_frame`
140+
141+
Decodes a raw CAN frame into named signals using a DBC database.
142+
143+
```python
144+
from slicks import load_dbc, decode_frame
145+
146+
db = load_dbc("path/to/WFR26.dbc")
147+
frame = decode_frame(db, can_id, raw_bytes) # → DecodedFrame or None
148+
```
149+
150+
**`DecodedFrame` fields:**
151+
- `message_name` *(str)*: CAN message name from the DBC.
152+
- `can_id` *(int)*: CAN frame ID.
153+
- `signals` *(dict[str, float])*: Decoded signal values.
154+
155+
### `slicks.frame_to_line_protocol`
156+
157+
Converts a `DecodedFrame` to an InfluxDB line protocol string.
158+
159+
```python
160+
from slicks import frame_to_line_protocol
161+
162+
line = frame_to_line_protocol(frame, measurement="WFR26", timestamp_ns=ts)
163+
```

docs/example_analysis.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,14 @@ Connecting to Slicks Telemetry Database...
2929

3030
## 2. Discovering Sensors
3131

32-
Before fetching data, we need to know exactly what sensors were recording during our test session. We'll scan a specific window to see the available signals.
32+
Before fetching data, we can check what sensors are available. With wide format, this is an instant metadata lookup — no time range needed.
3333

3434
```python
3535
start_time = datetime(2025, 9, 28, 20, 20, 0)
3636
end_time = datetime(2025, 9, 28, 21, 0, 0)
3737

38-
print(f"Scanning for sensors between {start_time} and {end_time}...")
39-
available_sensors = slicks.discover_sensors(start_time, end_time)
38+
# Wide format: instant column metadata lookup
39+
available_sensors = slicks.discover_sensors(start_time, end_time, schema="wide")
4040

4141
# Filter for Inverter (INV) related sensors to narrow our search
4242
inv_sensors = [s for s in available_sensors if s.startswith("INV_")]
@@ -45,8 +45,6 @@ print(f"Found {len(inv_sensors)} Inverter sensors. Examples: {inv_sensors[:5]}")
4545

4646
**Output:**
4747
```text
48-
Scanning for sensors between 2025-09-28 20:20:00 and 2025-09-28 21:00:00...
49-
Discovering sensors from 2025-09-28 20:20:00 to 2025-09-28 21:00:00...
5048
Discovery Complete. Found 342 unique sensors.
5149
Found 91 Inverter sensors. Examples: ['INV_Analog_Input_1', 'INV_Analog_Input_2', 'INV_Analog_Input_3', 'INV_Analog_Input_4', 'INV_Analog_Input_5']
5250
```
@@ -62,9 +60,14 @@ target_signals = ["INV_Motor_Speed", "INV_DC_Bus_Current"]
6260

6361
print(f"Fetching data for: {target_signals}...")
6462

65-
# Fetch 1-second resampled data.
63+
# Fetch 1-second resampled data in wide format.
6664
# We disable filter_movement to capture the full session including startup.
67-
df = slicks.fetch_telemetry(start_time, end_time, signals=target_signals, filter_movement=False)
65+
df = slicks.fetch_telemetry(
66+
start_time, end_time,
67+
signals=target_signals,
68+
filter_movement=False,
69+
schema="wide",
70+
)
6871

6972
if df is not None:
7073
print(f"Successfully loaded {len(df)} data points.")
@@ -77,8 +80,8 @@ Fetching data for: ['INV_Motor_Speed', 'INV_DC_Bus_Current']...
7780
Executing query for range: 2025-09-28 20:20:00 to 2025-09-28 21:00:00...
7881
Fetched 117 rows.
7982
Successfully loaded 117 data points.
80-
signalName INV_DC_Bus_Current INV_Motor_Speed
81-
time
83+
INV_DC_Bus_Current INV_Motor_Speed
84+
time
8285
2025-09-28 20:21:27 0.0 0.0
8386
2025-09-28 20:21:28 0.0 0.0
8487
2025-09-28 20:21:29 0.0 0.0

docs/getting_started.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ end = datetime(2025, 9, 28, 14, 0, 0) # Sept 28, 2025 at 02:00 PM
5454
You can request a single sensor or a list of sensors.
5555

5656
```python
57-
# Fetch Motor Speed
58-
df = slicks.fetch_telemetry(start, end, "INV_Motor_Speed")
57+
# Fetch Motor Speed (wide format — each signal is a column)
58+
df = slicks.fetch_telemetry(start, end, "INV_Motor_Speed", schema="wide")
5959

6060
if df is not None:
6161
print(df.head())

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "slicks"
7-
version = "0.2.0"
7+
version = "0.2.1"
88
description = "The home baked data pipeline for Western Formula Racing"
99
readme = "README.md"
1010
authors = [
@@ -21,6 +21,7 @@ dependencies = [
2121
"influxdb3-python>=0.1.0",
2222
"influxdb-client>=1.30.0",
2323
"cantools>=39.0.0",
24+
"httpx>=0.27.0",
2425
"python-dotenv>=1.0.0",
2526
"matplotlib>=3.0.0",
2627
"tqdm>=4.0.0",

0 commit comments

Comments
 (0)