1 /**
2  * This is the module which contains the Object-Oriented D wrapper.
3  */
4 module blink1;
5 
6 import core.thread;
7 
8 import std.algorithm;
9 import std.datetime;
10 import std.exception;
11 import std..string;
12 
13 import blink1.clib;
14 
15 /**
16  * General exception with a BLink1 device.
17  *
18  * Usually thrown when an IO error occurs with the USB device,
19  * which is usually caused by an user unplugging the blink(1) device.
20  */
21 class Blink1Exception : Exception {
22 	mixin basicExceptionCtors;
23 }
24 /**
25  * Thrown when no Blink1 could be found.
26  */
27 class Blink1NotFoundException : Blink1Exception {
28 	mixin basicExceptionCtors;
29 }
30 
31 /**
32  * Thrown when a device does not support the requested method. 
33  *
34  * This exception is for example thrown when the [Blink1Device.readNote] and [Blink1Device.writeNote] 
35  * functions are called on MK1 and MK2 devices, which do not support the note feature.
36  */
37 class UnsupportedOperationException : Blink1Exception {
38 	mixin basicExceptionCtors;
39 }
40 
41 /**
42  * Enum containing the different models of the blink(1) devices out there.
43  *
44  * Simple alias for the type found within the C library.
45  *
46  * See_Also: blink1.clib.blink1Type_t
47  */
48 alias Blink1Type = blink1Type_t;
49 
50 alias RGB = rgb_t;
51 
52 /**
53  * Represents the LED on the device.
54  */
55 enum LED {
56 	/// Special value: All LEDS
57 	ALL = 0,
58 	/// First LED
59 	ONE = 1,
60 	/// Second LED.
61 	TWO = 2
62 }
63 
64 /// Describes the boot mode of the MK2 and later devices.
65 enum BootMode {
66 	/// Play the stored pattern if only power is connected, otherwise play nothing.
67 	NORMAL = 0,
68 	/// Play the stored pattern
69 	PLAY = 1,
70 	/// Do not play anything
71 	OFF = 2
72 }
73 
74 /**
75  * Argument for controlling play behaviour.
76  */
77 enum PlayMode {
78 	STOP = 0, /// Stop playing the pattern
79 	PLAY = 1  /// Start playing the pattern.
80 }
81 
82 /// Represents the boot parameters
83 struct StartupParameters {
84 	BootMode bootMode;
85 	/// From which index the device should start playing.
86 	ubyte playStart;
87 	/// Until which index the device should play.
88 	ubyte playEnd;
89 	/// The amount of times it should play. 0 = infinite.
90 	ubyte playCount;
91 };
92 
93 /**
94  * Represents a pattern line.
95  */
96 struct PatternLine {
97 	/// The color of this pattern.
98     RGB color;
99 	/// Time in to transition towards this pattern.
100     Duration duration;
101     /// The led.
102     LED led;
103 }
104 
105 /// Playing state of the blink(1) LED
106 struct PlayState {
107 	/// Wether the blink(1) is playing or not.
108 	PlayMode playMode;
109 	/// Starting position of the pattern.
110 	ubyte playStart;
111 	/// The pattern position to stop playing at
112 	ubyte playEnd;
113 	/// Amount of times the pattern should be played. 0 = infinite
114 	ubyte playCount;
115 	/// Current playing position
116 	ubyte playPosition;
117 }
118 
119 /**
120  * Represents a physical device.
121  *
122  * To get a reference to a device, one should call either [open], [openByPath], [openBySerial] or
123  * [openByIndex]. After being done with the device, please call [close] to release OS resources.
124  */
125 class Blink1Device {
126 
127 
128 	/**
129 	 * The maximum amount of notes that this device may hold.
130 	 */
131 	// These are not static since they may change with new hardware revisions or firmware versions.
132 	public immutable maxNotes = 10;
133 	public immutable maxNoteSize = blink1_note_size;
134 
135 	/// Our "handle" to the blink led.
136 	protected blink1_device *m_device;
137 	/// The index of our device within the blink1 library's cache.
138 	protected int m_cachedIndex;
139 	/// The device type of our device;
140 	private immutable Blink1Type m_type;
141 	// Keep the firmware version around
142 	private int m_fwVersion;
143 	private immutable ubyte m_maxPatterns;
144 	private Duration m_defaultDur = dur!"msecs"(500);
145 	private bool m_blocking = false;
146 
147 	private Duration m_serverDownTimeout;
148 	private bool m_serverDownStayLit;
149 	private ubyte m_serverDownStartPosition;
150 	private ubyte m_serverDownEndPosition;
151 
152 	private this(blink1_device *device) {
153 		this.m_device = device;
154 		m_cachedIndex = blink1_getCacheIndexByDev(device);
155 		m_type = blink1_deviceTypeById(m_cachedIndex);
156 
157 		if(m_type == Blink1Type.BLINK1_MK1) {
158 			m_maxPatterns = 12;
159 		} else if (m_type == Blink1Type.BLINK1_MK2 || m_type == Blink1Type.BLINK1_MK3) {
160 			m_maxPatterns = 32;
161 		} else {
162 			m_maxPatterns = 0;
163 		}
164 		m_fwVersion = blink1_getVersion(m_device);
165 	}
166 
167 	/**
168 	 * Opens the first found default Blink1Device. No guarantees are given which one is found first.
169 	 *
170 	 * Throws: a Blink1NotFoundException if no device could be found.
171 	 */
172 	@trusted
173 	public static Blink1Device open() {
174 		blink1_device *dev = blink1_open();
175 		enforce!Blink1NotFoundException(dev != null, "Could not find default Blink1Device");
176 		return new Blink1Device(dev);
177 	}
178 
179 	/**
180 	 * Obtains a Blink1 device by the OS-specific path.
181 	 *
182 	 * Params:
183 	 *     path = The OS-specific path to the USB device.
184 	 *
185 	 * Throws: a Blink1NotFoundException if no device could be found with the given path.
186 	 */
187 	@trusted
188 	public static Blink1Device openByPath(string path) {
189 		blink1_device *dev = blink1_openByPath(path.toStringz());
190 		enforce!Blink1NotFoundException(dev != null, "Could not find Blink1Device with path %s".format(path));
191 		return new Blink1Device(dev);
192 	}
193 
194 	/**
195 	 * Opens the device by the given serial number.
196 	 *
197 	 * Params:
198 	 *     serial = The serial number of the device.
199 	 * Throws: a Blink1NotFoundException if no device could be found with the given serial number.
200 	 */
201 	@trusted
202 	public static Blink1Device openBySerial(string serial) {
203 		blink1_device *dev = blink1_openBySerial(serial.toStringz());
204 		enforce!Blink1NotFoundException(dev != null, "Could not find Blink1Device with serial %s".format(serial));
205 		return new Blink1Device(dev);
206 	}
207 
208 	/**
209 	 * Opens the device by the given index.
210 	 * 
211 	 * Params:
212 	 *     index = The index of the device. It should range from 0 to blink1_max_devices.
213 	 *
214 	 * Throws: a Blink1NotFoundException if no device could be found with the given index;
215 	 *
216 	 * See_Also: connectedDeviceCount
217 	 */
218 	@trusted
219 	public static Blink1Device openByIndex(uint index) {
220 		blink1_device *dev = blink1_openById(index);
221 		enforce!Blink1NotFoundException(dev != null, "Could not find Blink1Device with serial %u".format(index));
222 		return new Blink1Device(dev);
223 	}
224 
225 	/**
226 	 * Returns the amount of detected Blink1 devices.
227 	 */
228 	@trusted
229 	public static int connectedDeviceCount() {
230 		return blink1_enumerate();
231 	}
232 
233 	/**
234 	 * Closes this device.
235 	 */
236 	@trusted
237 	public void close() {
238 		blink1_close_internal(m_device);
239 	}
240 
241 	/**
242 	 * Returns the firmware version integer.
243 	 *
244 	 * The hundreds represent the major version, while the units and tenths represent the minor version,
245 	 * e.g. "v3.3" = 303;
246 	 */
247 	@safe
248 	public int getFwVersionInt() {
249 		return m_fwVersion;
250 	}
251 
252 	/**
253 	 * Returns the firmware version as a string in the form of v{major}.{minor}
254 	 */
255 	@safe
256 	public string getFwVersion() {
257 		int fwVersion = getFwVersionInt();
258 		int major = fwVersion / 100;
259 		int minor = fwVersion % 100;
260 		return "v%d.%d".format(major, minor);
261 	}
262 
263 	/**
264 	 * Fades the color to the specified value with the given duration.
265 	 *
266 	 * Params:
267 	 *     r = red component of color (0 <= r <= 255)
268 	 *     g = green component of color (0 <= g <= 255)
269 	 *     b = blue component of color (0 <= b <= 255)
270 	 *     dur = duration of the fade animation. Maximum duration is 65,355 milliseconds. Defaults to `defaultDuration`
271 	 *     led = which LED to fade. Defaults to all LEDs
272 	 *   
273 	 * See_Also: [defaultDuration], [setRGB], [fadeToHSV]
274 	 * Throws: Blink1Exception on error.
275 	 */
276 	@trusted
277 	public void fadeToRGB(ubyte r, ubyte g, ubyte b, Duration dur = seconds(-1), LED led = LED.ALL) {
278 		if (dur.isNegative) dur = this.m_defaultDur;
279 		throwIfFail(blink1_fadeToRGBN(m_device, cast(short) dur.total!"msecs", r, g, b, cast(ubyte) led));
280 		if (m_blocking) Thread.sleep(dur);
281 	}
282 
283 	/**
284 	 * Fades the color to the specified value with the given duration.
285 	 *
286 	 * Params:
287 	 *     h = hue component of color (0 <= h <= 255)
288 	 *     s = saturation component of color (0 <= s <= 255)
289 	 *     v = value component of color (0 <= v <= 255)
290 	 *     dur = duration of the fade animation. Maximum duration is 65,355 milliseconds. Defaults to `defaultDuration`
291 	 *     led = which LED to fade. Defaults to all LEDs
292 	 *   
293 	 * See_Also: [defaultDuration], [setHSV], [fadeToRGB]
294 	 * Throws: Blink1Exception on error.
295 	 */
296 	@safe
297 	public void fadeToHSV(ubyte h, ubyte s, ubyte v, Duration dur = seconds(-1), LED led = LED.ALL) {
298 		RGB color = fromHSV(h, s, v);
299 		fadeToRGB(color.r, color.g, color.b, dur, led);
300 	}
301 
302 	/**
303 	 * Immediatelty changes the color to the given value
304 	 *
305 	 * Params:
306 	 *     r = red component of color (0 <= r <= 255)
307 	 *     g = green component of color (0 <= g <= 255)
308 	 *     b = blue component of color (0 <= b <= 255)
309 	 *
310 	 * See_Also: [fadeToRGB], [setHSV]
311 	 * Throws: Blink1Exception on error.
312 	 */
313 	@trusted
314 	public void setRGB(ubyte r, ubyte g, ubyte b) {
315 		throwIfFail(blink1_setRGB(m_device, r, g, b));
316 	}
317 
318 	/**
319 	 * Immediatelty changes the color to the given value
320 	 *
321 	 * Params:
322 	 *     h = hue component of color (0 <= h <= 255)
323 	 *     s = saturation component of color (0 <= s <= 255)
324 	 *     v = value component of color (0 <= v <= 255)
325 	 *
326 	 * See_Also: [fadeToRGB], [setRGB]
327 	 * Throws: Blink1Exception on error.
328 	 */
329 	@safe
330 	public void setHSV(ubyte h, ubyte s, ubyte v) {
331 		RGB color = fromHSV(h, s, v);
332 		setRGB(color.r, color.g, color.b);
333 	}
334 
335 	/**
336 	 * Enables the blink1's dead man's trigger. This will play the pattern on the LED when `pokeServerDown` hasn't
337 	 * been called after `timeout` has been passed.
338 	 *
339 	 * Params:
340 	 *     timeout = when to start blinking.
341 	 *     stayLit = if the LED should stay on after playing the pattern.
342 	 *     startPosition = The starting position within the pattern. Default: 0
343 	 *     endPostion = The ending position of the pattern.
344 	 */
345 	@trusted
346 	public void enableServerDown(Duration timeout, bool stayLit, ubyte startPosition = 0, ubyte endPosition = 255) {
347 		if (endPosition == 255) endPosition = m_maxPatterns;
348 		this.m_serverDownTimeout = timeout;
349 		this.m_serverDownStayLit = stayLit;
350 		this.m_serverDownStartPosition = startPosition;
351 		this.m_serverDownEndPosition = endPosition;
352 		throwIfFail(blink1_serverdown(m_device,  1, cast(short) timeout.total!"msecs", cast(ubyte) stayLit, 
353 				startPosition, endPosition));
354 	}
355 
356 	/**
357 	 * Notifies the LED the "server" is still running, so it won't turn on. 
358 	 * 
359 	 * See_Also: enableServerDown
360 	 */
361 	@trusted
362 	public void pokeServerDown() {
363 		throwIfFail(blink1_serverdown(m_device, cast(ubyte) 1, cast(ushort) this.m_serverDownTimeout.total!"msecs", 
364 				cast(ubyte) this.m_serverDownStayLit, this.m_serverDownStartPosition, 
365 				this.m_serverDownEndPosition));
366 	}
367 
368 	/**
369 	 * Disables the serverDown mode. 
370 	 *
371 	 * See_Also: enableServerDown
372 	 */
373 	@trusted
374 	public void disableServerDown() {
375 		throwIfFail(blink1_serverdown(m_device, 0, 0, 0, 0, 0));
376 	}
377 
378 	/**
379 	 * Plays a stored pattern. The end and count parameters are only supported on MK2 and later
380 	 * devices.
381 	 *
382 	 * Params:
383 	 *     start = The pattern position to start playing from.
384 	 *     end =  The final pattern position to stop playing.
385 	 *     count = The amount of times to loop this pattern. 0 = infinite.
386 	 * See_Also: [stop]
387 	 * Throws: Blink1Exception if error
388 	 */
389 	@trusted
390 	public void play(ubyte start = 0, ubyte end = 255, ubyte count = 0) {
391 		throwIfFail(blink1_playloop(m_device, cast(ubyte) PlayMode.PLAY, start, end, count));
392 	}
393 
394 
395 	/// Ditto
396 	@trusted
397 	public void play(ubyte start = 0) {
398 		throwIfFail(blink1_play(m_device, cast(ubyte) PlayMode.PLAY, start));
399 	}
400 
401 	/**
402 	 * Stop playing whatever pattern the device is playing.
403 	 * See_Also: [play]
404 	 */
405 	@trusted
406 	public void stop() {
407 		throwIfFail(blink1_play(m_device, cast(ubyte) PlayMode.STOP, 255));
408 	}
409 
410 	/**
411 	 * Read the play state. Supported on MK2 devices and later.
412 	 *
413 	 * Throws: Blink1Exception if error
414 	 */
415 	@trusted
416 	public PlayState readPlayState() {
417 		PlayState state;
418 		throwIfFail(blink1_readPlayState(m_device, cast(ubyte*) &state.playMode, &state.playStart,
419 					&state.playEnd, &state.playCount, &state.playPosition));
420 		return state;
421 	}
422 
423 	/**
424 	 * Reads the stored pattern line at position `pos`.
425 	 *
426 	 * Params:
427 	 *     pos = The position of the pattern stored on the device, starting from 0
428 	 * Returns: The stored PatternLine
429 	 * Throws: Blink1Exception if an error occurs.
430 	 */
431 	@trusted
432 	public PatternLine readPatternLine(ubyte pos) {
433 		PatternLine result;
434 		ushort fadeMillis;
435 		if (getFwVersionInt >= 204) {
436 			throwIfFail(blink1_readPatternLineN(m_device, &fadeMillis, &result.color.r, 
437 						&result.color.g, &result.color.b, cast(ubyte*) &result.led, pos));
438 			
439 		} else {
440 			throwIfFail(blink1_readPatternLine(m_device, &fadeMillis, &result.color.r, 
441 						&result.color.g, &result.color.b, pos));
442 		}
443 		result.duration = msecs(fadeMillis);
444 		return result;
445 	}
446 
447 	/**
448 	 * Writes a pattern line to the device.
449 	 *
450 	 * On MK1 devices this will store it in nonvolatile storage.
451 	 * On MK2 and later devices this will store it in RAM. Call [savePattern] to store it in 
452 	 * nonvolatile storage.
453 	 * 
454 	 * Params:
455 	 *     pos = The position to write to, starting from zero.
456 	 *     patternLine = The patternLine to store.
457 	 * Throws: Blink1Exception if an error occurs.
458 	 */
459 	@trusted
460 	public void writePatternLine(ubyte pos, PatternLine patternLine) {
461 		writePatternLineRGB(pos, patternLine.color.r, patternLine.color.g, patternLine.color.b, 
462 				patternLine.duration, patternLine.led);
463 	}
464 
465 	/**
466 	 * Writes a pattern line to the device.
467 	 *
468 	 * On MK1 devices this will store it in nonvolatile storage.
469 	 * On MK2 and later devices this will store it in RAM. Call [savePattern] to store it in 
470 	 * nonvolatile storage.
471 	 * 
472 	 * Params:
473 	 *     pos = The position to write to, starting from zero.
474 	 *     r = red component
475 	 *     g = green component
476 	 *     b = blue component
477 	 *     duration = time it takes to fade
478 	 *     led = The LED to use.
479 	 * Throws: Blink1Exception if an error occurs.
480 	 */
481 	@trusted
482 	public void writePatternLineRGB(ubyte pos, ubyte r, ubyte g, ubyte b, Duration duration = seconds(01), 
483 			LED led = LED.ALL) {
484 		if (duration.isNegative) duration = m_defaultDur;
485 
486 		if (getFwVersionInt >= 204) {
487 			throwIfFail(blink1_setLEDN(m_device, cast(ubyte) led));
488 		}
489 		throwIfFail(blink1_writePatternLine(m_device, cast(ushort) duration.total!"msecs",
490 				r, g, b, pos));
491 	}
492 
493 	/**
494 	 * Writes a pattern line to the device.
495 	 *
496 	 * On MK1 devices this will store it in nonvolatile storage.
497 	 * On MK2 and later devices this will store it in RAM. Call [savePattern] to store it in 
498 	 * nonvolatile storage.
499 	 * 
500 	 * Params:
501 	 *     pos = The position to write to, starting from zero.
502 	 *     h = hue component
503 	 *     s = saturation component
504 	 *     v = value component
505 	 *     duration = time it takes to fade
506 	 *     led = The LED to use.
507 	 * Throws: Blink1Exception if an error occurs.
508 	 */
509 	@trusted
510 	public void writePatternLineHSV(ubyte pos, ubyte h, ubyte s, ubyte v, Duration duration = seconds(01), 
511 			LED led = LED.ALL) {
512 		RGB color = fromHSV(h, s, v);
513 		writePatternLineRGB(pos, color.r, color.g, color.b, duration, led);
514 	}
515 
516 	/**
517 	 * Saves the pattern in the device's ram to nonvolatile memory.
518 	 * Only works on MK2 devices with firmware 204 or later, or later devices.
519 	 *
520 	 * Throws: Blink1Exception if error
521 	 */
522 	@trusted
523 	public void savePattern() {
524 		throwIfFail(blink1_savePattern(m_device));
525 	}
526 
527 	/**
528 	 * Reads the startup parameters of the device.
529 	 *
530 	 * Throws: Blink1Exception if error
531 	 */
532 	@trusted
533 	public StartupParameters getStartupParameters() {
534 		StartupParameters params;
535 		throwIfFail(blink1_getStartupParams(m_device, cast(ubyte*) &params.bootMode,
536 				&params.playStart, &params.playEnd, &params.playCount));
537 		return params;
538 	}
539 
540 
541 
542 	/**
543 	 * Sets the startup parameters
544 	 * 
545 	 * Params:
546 	 *     bootMode = the boot mode
547 	 *     playStart = From which stored pattern line to start playing from
548 	 *     playEnd =  To which stored pattern line to play to
549 	 *     playCount = How many times to repeat the pattern. 0 = infinite.
550 	 *
551 	 * Throws: Blink1Exception if error
552 	 */
553 	@trusted
554 	public void setStartupParameters(BootMode bootMode, ubyte playStart, ubyte playEnd, ubyte playCount) {
555 		throwIfFail(blink1_setStartupParams(m_device, cast(ubyte) bootMode, playStart, playEnd, playCount));
556 	}
557 
558 	/// Ditto
559 	@safe
560 	public void setStartupParameters(StartupParameters params) {
561 		setStartupParameters(params.bootMode, params.playStart, params.playEnd, params.playCount);
562 	}
563 
564 	/**
565 	 * Reads a note from the device.
566 	 *
567 	 * Params:
568 	 *     id = The id of the note, 0 <= id < maxNotes
569 	 * Returns: The read note
570 	 * Throws: UnsupportedOperationException if id < [maxNotes]
571 	 */
572 	@trusted
573 	public ubyte[] readNote(ubyte id) {
574 		enforce!UnsupportedOperationException(m_type == Blink1Type.BLINK1_MK3);
575 		enforce!Exception(id < maxNotes, "Device supports up to %d notes, tried to read %d".format(maxNotes, id));
576 		ubyte[] noteBuffer = new ubyte[maxNoteSize];
577 		ubyte * noteBufferPtr = noteBuffer.ptr;
578 		blink1_readNote(m_device, id, &noteBufferPtr);
579 
580 		return noteBuffer;
581 	}
582 
583 	/**
584 	 * Writes a note with the given id.
585 	 *
586 	 * Params:
587 	 *     id = The id of the note to write to, 
588 	 *     note = The note to write. Maximum length is maxNoteSize. Longer notes will be silently
589 	 *            truncated.
590 	 */
591 	@trusted
592 	public void writeNote(ubyte id, immutable ubyte[] note) {
593 		// blink1_writeNote will happilly write garbage, try to sanitize it
594 		// by setting remaining data to zero
595 		enforce!UnsupportedOperationException(m_type == Blink1Type.BLINK1_MK3);
596 		enforce!Exception(id < maxNotes, "Device supports up to %d notes, tried to write %d".format(maxNotes, id));
597 		ulong length = min(maxNoteSize, note.length);
598 		ubyte[maxNoteSize] realNote;
599 		realNote[0..length] = note[0..length];
600 		blink1_writeNote(m_device, id, realNote.ptr);
601 	}
602 
603 	/**
604 	 * Sets the default duration for fade animations if no duration parameter is given.
605 	 */
606 	@safe
607 	@property void defaultDuration(Duration dur) {
608 		this.m_defaultDur = dur;
609 	}
610 
611 	/**
612 	 * Gets the default duration for fade animations if no duration parameter is given.
613 	 */
614 	@safe
615 	@property Duration defaultDuration() {
616 		return this.m_defaultDur;
617 	}
618 
619 	/**
620 	 * Enables/disables blocking mode
621 	 *
622 	 * If blocking mode is enabled, calls to `fadeToRGB` will sleep the thread until the
623 	 * animation is finished. If blocking mode is disabled, `fadeToRGB` will immediatly return.
624 	 */
625 	@safe
626 	@property void blocking(bool blocking) {
627 		this.m_blocking = blocking;
628 	}
629 
630 	/**
631 	 * Returns if blocking mode is enabled. 
632 	 *
633 	 * See_Also: blocking(bool)
634 	 */
635 	@safe
636 	@property bool blocking() {
637 		return this.m_blocking;
638 	}
639 
640 	/**
641 	 * Gets the device type of this device.
642 	 */
643 	@safe
644 	@property Blink1Type type() {
645 		return this.m_type;
646 	}
647 
648 	/**
649 	 * The serial number of this device.
650 	 */
651 	@trusted
652 	@property string serial() {
653 		import std.conv;
654 		char* str = blink1_getCachedSerial(this.m_cachedIndex);
655 		return fromStringz(str).to!string;
656 	}
657 
658 	/**
659 	 * The path of this device
660 	 */
661 	@trusted
662 	@property string path() {
663 		import std.conv;
664 		char* str = blink1_getCachedPath(this.m_cachedIndex);
665 		return fromStringz(str).to!string;
666 	}
667 
668 	/**
669 	 * Returns the maximum amount of patterns this device may hold.
670 	 */
671 	@safe
672 	@property int maxPatterns() {
673 		return m_maxPatterns;
674 	}
675 
676 	/*
677 	 * Private methods
678 	 */
679 
680 	/**
681 	 * Throws an exception if `result < 0`.
682 	 * Params:
683 	 *     result = The result code of the operation.
684 	 * Throws: E
685 	 */
686 	@trusted
687 	private void throwIfFail(E = Blink1Exception)(int result) {
688 		import std.conv;
689 		enforce!E(result >= 0, fromStringz(blink1_error_msg(result)).to!string());
690 	}
691 
692 	/**
693 	 * Converts HSV to RGB
694 	 */
695 	@trusted
696 	private static RGB fromHSV(ubyte h, ubyte s, ubyte v) {
697 		RGB color;
698 		ubyte[] tmp = [h, s, v];
699 		hsbtorgb(&color, tmp.ptr);
700 		return color;
701 	}
702 };