Skip to content

Commit 711cea4

Browse files
authored
Merge 513febc into 0fa8b31
2 parents 0fa8b31 + 513febc commit 711cea4

File tree

3 files changed

+610
-0
lines changed

3 files changed

+610
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ Version History
121121
| v0.2.0 | Dec 31, 2010 | CENTER 300, 301, 302, 303, 304, 305, 306, VOLTCRAFT K202, K204 300K, 302KJ, EXTECH 421509 |
122122
| v0.1.0 | Dec 20, 2010 | Initial release |
123123

124+
测试
125+
124126

125127
----
126128
License
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
#
2+
# ABOUT
3+
# Kaleido CSV roast profile importer for Artisan
4+
5+
import csv
6+
import re
7+
import logging
8+
from collections.abc import Callable
9+
from typing import Final
10+
11+
from artisanlib.util import encodeLocal, weight_units, weight_units_lower
12+
from artisanlib.atypes import ProfileData
13+
14+
_log: Final[logging.Logger] = logging.getLogger(__name__)
15+
16+
17+
# returns a dict containing all profile information contained in the given Kaleido CSV file
18+
def extractProfileKaleidoCSV(file: str,
19+
_etypesdefault: list[str],
20+
alt_etypesdefault: list[str],
21+
_artisanflavordefaultlabels: list[str],
22+
eventsExternal2InternalValue: Callable[[int], float]) -> ProfileData:
23+
res: ProfileData = ProfileData() # the interpreted data set
24+
25+
# Initialize data list
26+
timex = [] # Timeline
27+
temp1 = [] # ET
28+
temp2 = [] # BT
29+
temp3 = [] # RoR
30+
31+
specialevents = [] # Special event time points
32+
specialeventstype = [] # Event Type (0=Fan, 1=Drum, 2=Damper, 3=Burner)
33+
specialeventsvalue = [] # Event value
34+
specialeventsStrings = [] # Event Description
35+
36+
timeindex = [-1, 0, 0, 0, 0, 0, 0, 0] # Time Index: [CHARGE, DRY END, FC START, FC END, SC START, SC END, DROP, COOL]
37+
# CHARGE index init set to -1 as 0 could be an actual index used
38+
39+
# Analysis Kaleido CSV File(Kaleido The file is in text format and contains multiple sections.)
40+
with open(file, encoding='utf-8') as f:
41+
content = f.read()
42+
43+
# Split file content into different sections
44+
sections = {}
45+
current_section = None
46+
lines = content.split('\n')
47+
48+
i = 0
49+
while i < len(lines):
50+
line = lines[i].strip()
51+
52+
# Check if it is a section tag, such as [{DATA}], [{EVENT}], [{CookDate}] ...
53+
if line.startswith('[{') and line.endswith('}]'):
54+
current_section = line[2:-2] # Remove [{ and }]
55+
sections[current_section] = []
56+
i += 1
57+
continue
58+
59+
# If currently in a certain section Inside, collecting content
60+
if current_section and line:
61+
sections[current_section].append(line)
62+
63+
i += 1
64+
65+
# Analyze basic information
66+
# Analyzing the baking date and time
67+
if 'CookDate' in sections and sections['CookDate']:
68+
cook_date = sections['CookDate'][0].strip() if sections['CookDate'] else ''
69+
if cook_date:
70+
# Format: 25-05-18 19:32:48
71+
try:
72+
date_part, time_part = cook_date.split(' ')
73+
year = f"20{date_part[:2]}"
74+
month = date_part[3:5]
75+
day = date_part[6:8]
76+
res['roastdate'] = f"{year}-{month}-{day}"
77+
res['roastisodate'] = f"{year}-{month}-{day}"
78+
res['roasttime'] = time_part
79+
except:
80+
_log.warning('Could not parse CookDate: %s', cook_date)
81+
82+
# Analyze comments/titles
83+
if 'Comment' in sections and sections['Comment']:
84+
comment = sections['Comment'][0].strip() if sections['Comment'] else ''
85+
if comment:
86+
res['title'] = comment
87+
88+
# Analyzing preheating temperature
89+
if 'PreTemp' in sections and sections['PreTemp']:
90+
pre_temp = sections['PreTemp'][0].strip() if sections['PreTemp'] else ''
91+
if pre_temp:
92+
try:
93+
res['drumtemp'] = float(pre_temp) # Use the preheating temperature as the initial drum temperature
94+
except ValueError:
95+
pass
96+
97+
# Parse DATA section
98+
if 'DATA' in sections:
99+
data_lines = sections['DATA']
100+
101+
# First line is header
102+
if data_lines:
103+
headers = [h.strip() for h in data_lines[0].split(',')]
104+
105+
# Store previous parameter values to detect changes
106+
last_fan = None # Corresponds to SM (Fan/Air) -> Fan (index 0)
107+
last_drum = None # Corresponds to RL (Rotation) -> Drum (index 1)
108+
last_burner = None # Corresponds to HP (Heat Power) -> Burner (index 3)
109+
110+
# Prepare lists for extra device data
111+
sm_list = [] # SM (Fan/Air) -> Fan %
112+
rl_list = [] # RL (Rotation) -> Drum %
113+
hp_list = [] # HP (Heat Power) -> Burner %
114+
sv_list = [] # SV (Set Value) -> Set Value
115+
# hpm_list = [] # HPM (Manual/Auto mode) -> Mode (not displayed in Roast Properties Data)
116+
# ps_list = [] # PS (Status) -> Status (not displayed in Roast Properties Data)
117+
118+
for idx, line in enumerate(data_lines[1:]): # Skip the header row
119+
line = line.strip()
120+
if line:
121+
parts = line.split(',')
122+
if len(parts) >= 11: # Ensure there are enough columns
123+
try:
124+
# 解析数据列: Index,Time,BT,ET,RoR,SV,HPM,HP,SM,RL,PS
125+
# Index = parts[0] (跳过)
126+
time_ms = int(parts[1]) # Time (milliseconds)
127+
bt = float(parts[2]) # BT
128+
et = float(parts[3]) # ET
129+
ror = float(parts[4]) # RoR
130+
sv = float(parts[5]) # Set value
131+
132+
hpm_str = parts[6].strip() # HPM (Manual/Auto Mode - M=Manual Heating, A=PID Heating Control based on SV value)
133+
hp_str = parts[7].strip() # HP (Burner)
134+
sm_str = parts[8].strip() # SM (Air damper setting)
135+
rl_str = parts[9].strip() # RL (RPM)
136+
ps_str = parts[10].strip() # PS (Burner Status - O OR C)
137+
138+
# Conversion time (milliseconds to seconds)
139+
time_sec = time_ms / 1000.0
140+
141+
# Convert each parameter value
142+
hpm = hpm_str if hpm_str else 'M' # Default to manual mode
143+
hp = float(hp_str) if hp_str and hp_str not in ['0', ''] else 0.0
144+
sm = float(sm_str) if sm_str and sm_str not in ['0', ''] else 0.0
145+
rl = float(rl_str) if rl_str and rl_str not in ['0', ''] else 0.0
146+
ps = ps_str if ps_str else 'O' # Default to firepower on.
147+
148+
# Add to the data list
149+
timex.append(time_sec)
150+
temp1.append(et)
151+
temp2.append(bt)
152+
temp3.append(ror)
153+
154+
# Add to extra device data lists
155+
sm_list.append(sm)
156+
rl_list.append(rl)
157+
hp_list.append(hp)
158+
sv_list.append(sv)
159+
# HPM: M=Manual Heat (100), A=PID Heat Control based on SV value (0) - Kaleido machine mode, not displayed in Roast Properties Data
160+
# hpm_numeric = 100 if hpm == 'M' else 0
161+
# hpm_list.append(hpm_numeric) - HPM data not added to extra device list
162+
# PS: O=Heat On (100), C=Heat Off (0) - Kaleido machine specific, not displayed in Roast Properties Data
163+
# ps_numeric = 100 if ps == 'O' else 0
164+
# ps_list.append(ps_numeric) - PS data not added to extra device list
165+
166+
# Detect Fan (SM - Fan/Air) changes - Map to Artisan Fan (index 0)
167+
if sm != last_fan:
168+
last_fan = sm
169+
specialeventsvalue.append(eventsExternal2InternalValue(int(sm)))
170+
specialevents.append(idx)
171+
specialeventstype.append(0) # Fan
172+
specialeventsStrings.append(f'SM={int(sm)}%')
173+
174+
# Detect Drum (RL - Rotation) changes - Map to Artisan Drum (index 1)
175+
if rl != last_drum:
176+
last_drum = rl
177+
specialeventsvalue.append(eventsExternal2InternalValue(int(rl)))
178+
specialevents.append(idx)
179+
specialeventstype.append(1) # Drum
180+
specialeventsStrings.append(f'RL={int(rl)}%')
181+
182+
# Detect Burner (HP - Heat Power) changes - Map to Artisan Burner (index 3)
183+
if hp != last_burner:
184+
last_burner = hp
185+
specialeventsvalue.append(eventsExternal2InternalValue(int(hp)))
186+
specialevents.append(idx)
187+
specialeventstype.append(3) # Burner
188+
specialeventsStrings.append(f'HP={int(hp)}%')
189+
190+
except (ValueError, IndexError) as e:
191+
# Skip unparsable lines
192+
_log.warning('Could not parse data line: %s, error: %s', line, str(e))
193+
continue
194+
195+
# Add extra device data - Include all Kaleido parameters except HPM and PS
196+
if timex: # Ensure there is data
197+
res['extradevices'] = [32, 33, 44, 45] # Device IDs, removed HPM and PS device IDs
198+
res['extraname1'] = ['SM', 'RL', 'HP', 'SV'] # Removed HPM and PS
199+
res['extraname2'] = ['Fan', 'Drum', 'Burner', 'SV'] # Removed HPM and PS
200+
res['extratimex'] = [timex, timex, timex, timex] # Removed HPM and PS time axis
201+
res['extratemp1'] = [sm_list, rl_list, hp_list, sv_list] # Removed HPM and PS data
202+
res['extratemp2'] = [[0]*len(timex), [0]*len(timex), [0]*len(timex), [0]*len(timex)] # Removed HPM and PS data
203+
204+
# Parse event timestamps and map to Artisan timeindex
205+
# Mapping relationship:
206+
# StartBeansIn -> CHARGE (timeindex[0])
207+
# TurntoYellow -> DRY END (timeindex[1])
208+
# 1stBoomStart -> FC START (timeindex[2])
209+
# 1stBoomEnd -> FC END (timeindex[3])
210+
# 2ndBoomStart -> SC START (timeindex[4])
211+
# 2ndBoomEnd -> SC END (timeindex[5])
212+
# BeansColdDown -> DROP (timeindex[6])
213+
214+
event2timeindex = {
215+
'StartBeansIn': 0, # CHARGE
216+
'TurntoYellow': 1, # DRY END
217+
'1stBoomStart': 2, # FC START
218+
'1stBoomEnd': 3, # FC END
219+
'2ndBoomStart': 4, # SC START
220+
'2ndBoomEnd': 5, # SC END
221+
'BeansColdDown': 6 # DROP
222+
}
223+
224+
# Process events
225+
for event_name, time_idx in event2timeindex.items():
226+
if event_name in sections:
227+
event_data = sections[event_name][0] if sections[event_name] else ''
228+
if event_data:
229+
# Parse format like "170@00:00" -> temperature@time
230+
if '@' in event_data:
231+
try:
232+
time_str = event_data.split('@')[1]
233+
time_parts = time_str.split(':')
234+
if len(time_parts) == 2:
235+
minutes = int(time_parts[0])
236+
seconds = int(time_parts[1])
237+
time_seconds = minutes * 60 + seconds
238+
# Find closest time index
239+
if timex: # Ensure timex is not empty
240+
closest_idx = min(range(len(timex)), key=lambda i: abs(timex[i] - time_seconds))
241+
timeindex[time_idx] = closest_idx
242+
except (ValueError, IndexError) as e:
243+
_log.warning('Could not parse event time for %s: %s, error: %s', event_name, event_data, str(e))
244+
245+
# Set basic roast information
246+
res['samplinginterval'] = 1.5 # Kaleido CSV sampling interval is 1.5 seconds
247+
res['mode'] = 'C' # Default Celsius
248+
249+
# Set roast data
250+
res['timex'] = timex
251+
res['temp1'] = temp1
252+
res['temp2'] = temp2
253+
if temp3 and all(x != 0 for x in temp3): # If RoR data exists and not all zeros
254+
res['temp3'] = temp3
255+
256+
res['timeindex'] = timeindex
257+
258+
# Set special events (if exist)
259+
if len(specialevents) > 0:
260+
res['specialevents'] = specialevents
261+
res['specialeventstype'] = specialeventstype
262+
res['specialeventsvalue'] = specialeventsvalue
263+
res['specialeventsStrings'] = specialeventsStrings
264+
265+
# Set event type names
266+
# Artisan event type system:
267+
# Index 0: Fan
268+
# Index 1: Drum
269+
# Index 2: Damper
270+
# Index 3: Burner
271+
# Index 4: '--' (Special events/annotations for marking notes during roasting)
272+
#
273+
# Kaleido has only 3 control events, using 3 from Artisan:
274+
# SM (Fan/Air) -> Fan (0)
275+
# RL (Rotation) -> Drum (1)
276+
# HP (Heat Power) -> Burner (3)
277+
# HPM (M=Manual Heat, A=PID Heat Control based on SV value) - Kaleido machine mode, not used as Artisan control event
278+
# PS (Status) - Kaleido machine specific, not mapped to Artisan control event
279+
alt_etypesdefault[0] = 'Fan' # Corresponds to SM
280+
alt_etypesdefault[1] = 'Drum' # Corresponds to RL
281+
alt_etypesdefault[2] = 'Damper' # Reserved Damper label, but Kaleido doesn't use
282+
alt_etypesdefault[3] = 'Burner' # Corresponds to HP
283+
alt_etypesdefault[4] = '--' # Special events/annotations
284+
res['etypes'] = alt_etypesdefault
285+
286+
# Set roaster information
287+
res['roastertype'] = 'Kaleido Legacy'
288+
res['roastersize'] = 0.1 # Default roaster size
289+
290+
# Parse total time
291+
if 'TotalTime' in sections and sections['TotalTime']:
292+
total_time = sections['TotalTime'][0].strip() if sections['TotalTime'] else ''
293+
if total_time:
294+
try:
295+
time_parts = total_time.split(':')
296+
if len(time_parts) == 2:
297+
minutes = int(time_parts[0])
298+
seconds = int(time_parts[1])
299+
total_seconds = minutes * 60 + seconds
300+
res['roasttotaltime'] = total_seconds
301+
except ValueError:
302+
pass
303+
304+
return res

0 commit comments

Comments
 (0)