aboutsummaryrefslogtreecommitdiffstats
path: root/usr/space_light/src/space_light.c
blob: 807d9d0b86b3a4e0b19b908fe9856ad69f79c9b3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/*
 * Copyright (c) 2025 Bernhard Guillon <Bernhard.Guillon@begu.org>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the " Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * SPDX: MIT
 */

#include "comm.h"
#include "comm_task.h"
#include <math.h>
#include <space_light.h>
#include <stdint.h>

#include "FreeRTOS.h"
#include "task.h"
#include "timers.h"

#include "BkDriverPwm.h"

#define WARM BK_PWM_0
#define COLD BK_PWM_2
#define RED BK_PWM_3
#define GREEN BK_PWM_4
#define BLUE BK_PWM_5

TimerHandle_t xOneShotTimer = NULL;

void power_save_mode() {
  // Turn your lights down low
  // And pull your window curtain (yeah)
  // Couldn't resitst ^^
  bk_pwm_initialize(RED, 26000, 26000);
  bk_pwm_start(RED);
  bk_pwm_initialize(GREEN, 26000, 26000);
  bk_pwm_start(GREEN);
  bk_pwm_initialize(BLUE, 26000, 26000);
  bk_pwm_start(BLUE);
  bk_pwm_initialize(COLD, 26000, 26000);
  bk_pwm_start(COLD);
  bk_pwm_initialize(WARM, 26000, 0);
  bk_pwm_start(WARM);
}

/*
 * As the following took me longer then expected
 * I will keep this note for the future me or the
 * interested reader.
 *
 * The red green and blue LED are each connected to a
 * OCI OC7140 LED PWM dimmer chip.
 *
 *  5678
 *  ||||
 * [7140]  2 PWM dimmer input  4 LED output
 *  ||||
 *  1234
 *
 * This chip is driven by the (RED|GREEN|BLUE) PWM signal.
 * The recomanded frequency for the dimmer is within 1000KHz
 * where low -> LED off -> high -> LED on.
 *
 * I debuged the PWM and dimmer behavior with my logic analyzer
 * changing the frequency comparator of one PWM channel
 * sometimes messes up with the other channels. I gave
 * up on when exactly this happens. But the good news is
 * changing the duty comparotor on the other hand seems to
 * be isolated to the PWM channel :)
 *
 * Make sure to set the initial and start the pwm in a seperate
 * init step.
 *
 * Afterwards only use the update function and never try to turn
 * the PWM chip off and change the GPIO input source and try to
 * set it manually and reapply the PWM later. As this also messes
 * the other channels. As we don't know how to enter to a propper
 * powersave mode with the control pin as wakeup source anywas
 * the BLE will probalby drain the most power ^^. After figguring
 * out how to proper shut down the BLE we can use the control button
 * to call init and "power_save_mode".
 *
 * Therefore to keep things simple we set the PWM frequency to
 * 1000Khz and use the duty cycle to generate a positive perioid of
 * 0us to 1000us. The chips interface takes a number between 0 and
 * 1000 anyways for the "end_value" which they otherwise call duty
 * cycle between 0-100 in 0.1% steps. This is perfect for us to
 * finaly being able to fully controll the LEDs as setting this to 0
 * realy gives us a low signal without any highs. As low and hight
 * are inverted we nedd to apply 1000-positive_period.
 *
 * We now take the RGB values which are between 0..255 and multiply
 * them by 4 and finaly have the RGB spectrum with full on and off
 *
 * The only thing left is that we need to apply gamma correction
 * to match the human eye.
 *
 * We leave it to the client application to create a HUE Ring
 * to nicely controll our device.
 *
 * We also controll the motor and the laser with this interface
 * as it seems to be the most common way to use RGBCW to do this.
 * Most applications use values from 0..100 instead of 0..255 for
 * CW our application uses cool (C) for the laser and warm (W)
 * for the motor.
 *
 * For the motor we need to generate a positive_period from
 * 0..1000. The motor start to drive smothly at 200 and seems
 * to be linear. Therefore we need to use the range between
 * 200.1000 with 0 being 0 and 200 being 1. As the output
 * is inverted we also need to apply 1000-positive_period.
 *
 * For the laser we need to generate a positive_period from
 * 0..1000. The laser starts to shine at 200 and seems
 * to be linear. Therefore we need to use the range between
 * 200.1000 with 0 being 0 and 200 being 1.
 *
 */
void set_rgbcw(struct RGBCW *rgbcw) {
  const float clk_26M = 26000000.0f;
  const float perioid_us = 1000.0f;
  const float pwm_period = clk_26M / perioid_us;

  // RGB control
  const float red_gamma_max = 1000.0f;  // 100%
  const float green_gamma_max = 700.0f; // 70%
  const float blue_gamma_max = 750.0f;  // 75%
  const float gamma = 2.2f;

  float r_val_gamma =
      pow((float)rgbcw->r / 255.0f, gamma) * red_gamma_max + 0.5f;
  float r_val_inv = 1000.f - r_val_gamma;
  float r_period = r_val_inv / 1000.0f * pwm_period;
  bk_pwm_update_param(RED, (uint16_t)pwm_period, (uint16_t)r_period);

  float g_val_gamma =
      pow((float)rgbcw->g / 255.0f, gamma) * green_gamma_max + 0.5f;
  float g_val_inv = 1000.f - g_val_gamma;
  float g_period = g_val_inv / 1000.0f * pwm_period;
  bk_pwm_update_param(GREEN, (uint16_t)pwm_period, (uint16_t)g_period);

  float b_val_gamma =
      pow((float)rgbcw->b / 255.0f, gamma) * blue_gamma_max + 0.5f;
  float b_val_inv = 1000.f - b_val_gamma;
  float b_period = b_val_inv / 1000.0f * pwm_period;
  bk_pwm_update_param(BLUE, pwm_period, b_period);

  // Laser control
  uint16_t laser_power = rgbcw->c; //& 0x64
  uint16_t l_period = 0;
  // FIMXE: use bitmaks but the compiler should guess this right
  if (laser_power > 100) {
    laser_power =
        100; // Make sure 100 is the upper as we want to use percentage
  }
  if (laser_power != 0) {
    uint16_t one_percent = 800 / 100;
    uint16_t lower_limit = 200;
    l_period = laser_power * one_percent + lower_limit;
    l_period = 1000.f - l_period; // we need to invert it
    l_period = (float)l_period / 1000.0f * pwm_period;
  }
  bk_pwm_update_param(COLD, pwm_period, l_period);

  // Motor control
  uint16_t motor_speed = rgbcw->w; //& 0x64
  uint16_t m_period = 0;
  // FIMXE: use bitmaks but the compiler should guess this right
  if (motor_speed > 100) {
    motor_speed =
        100; // Make sure 100 is the upper as we want to use percentage
  }
  if (motor_speed != 0) {
    uint16_t one_percent = 800 / 100;
    uint16_t lower_limit = 200;
    m_period = motor_speed * one_percent + lower_limit;
    m_period = (float)m_period / 1000.0f * pwm_period;
  }
  bk_pwm_update_param(WARM, pwm_period, m_period);
}

static void oneShotTimerCallback(TimerHandle_t xTimer) { power_save_mode(); }

void set_timer(uint16_t seconds) {
  // FIXME: restructure the code
  if (xOneShotTimer == NULL && seconds == 0) {
    return;
  }
  if (seconds == 0) {
    xTimerStop(xOneShotTimer, 0);
  }

  if (xOneShotTimer == NULL) {
    xOneShotTimer = xTimerCreate("OneShot", seconds * 1000, pdFALSE, 0,
                                 oneShotTimerCallback);
  }
  if ((xOneShotTimer != NULL)) {
    xTimerStart(xOneShotTimer, 0);
  }
}

// Complete hacky command which will be removed shortly
// this is just to shorten the developemnt cycle
void setPWM(uint8_t *values, size_t len) {
  // at least try something ^^

  switch (values[0]) {
  case 0x01: // initialzie
  {
    if (len < 6) {
      return;
    }
    uint8_t pwm = values[1];
    uint16_t frequency = values[2] << 8 | values[3];
    uint16_t duty = values[4] << 8 | values[5];
    bk_pwm_initialize(pwm, frequency, duty);
    break;
  }
  case 0x02: // start
  {
    if (len < 2) {
      return;
    }
    uint8_t pwm = values[1];
    bk_pwm_start(pwm);
    break;
  }
  case 0x03: // stop
  {
    if (len < 2) {
      return;
    }
    uint8_t pwm = values[1];
    bk_pwm_stop(pwm);
    break;
  }
  case 0x04: // update param
  {
    if (len < 6) {
      return;
    }
    uint8_t pwm = values[1];
    uint16_t frequency = values[2] << 8 | values[3];
    uint16_t duty = values[4] << 8 | values[5];
    bk_pwm_update_param(pwm, frequency, duty);
  }
  default:
    return;
  }
}

void init_led_thread(void *arg) {
  power_save_mode();
  rtos_delete_thread(NULL);
}