Category: <span>Front Page</span>

Cubecell HTCC-AB02S

CubeCell HTCC-AB02S

HTCC-AB02S is a Dev-Board. Already integrated AIR530Z GPS module, friendly designed for developers, easy to verify communication solutions.


I found this board by browsing around for an integrated solution that has GPS, Radio, and display capabilities. Getting the libraries working took some effort, but it has been worth it.

The CubeCell HTCC-AB02S is LoRa capable out-of-the-box, and eventually I will invest more time in understanding the proper usage of the protocol. In my project I took a very naive approach, please be advised this is probably not an optimal solution.

I purchased two of these boards, one to act as a beacon/transmitter that chirps out it’s current GPS coordinates every few seconds. The second board I programmed as a receiver and it computes the distance and bearing to the beacon and displays information on the LCD screen.

Product Link

ezradio.h
#include "Arduino.h"
#include "LoRaWan_APP.h"

#define LORA_PREAMBLE_LENGTH                8               // Same for Tx and Rx
#define LORA_FIXED_PAYLOAD                  false
#define LORA_CRC                            true
#define TX_OUTPUT_POWER                     20 // 14        // dBm (max = 22).
#define LORA_SYMBOL_TIMEOUT                 0               // Symbols
#define LORA_USE_FIXED_PAYLOAD           false
#define LORA_IQ_INVERSION                false
#define LORA_FREQ_HOP                    false

// radio transmission data
struct RadioPacket {
  int16_t id;
  uint8_t from;
  uint8_t type;
  int16_t rssi;
  int8_t snr;
  uint8_t signalStrength;
  uint32_t at;
  double lat;
  double lon;
  uint32_t age;
  int length;
  char data[64];
};

struct EzRadioCfg {
  RadioModems_t modem = MODEM_LORA;
  int8_t power = 18;  // max 22

  uint8_t fdev = 0;  // FSK Modem Only. Sets the frequency deviation (FSK only)

  // LORA_BANDWIDTH
  //  0: 125 kHz,
  //  1: 250 kHz,
  //  2: 500 kHz,
  //  3: Reserved
  byte bandwidth = 0;

  // LORA_SPREADING_FACTOR
  // min/default 7, max 12          [SF7..SF12]
  byte datarate = 7;

  // LORA_CODINGRATE
  //  1: 4/5,
  //  2: 4/6,
  //  3: 4/7,
  //  4: 4/8
  uint8_t coderate = 1;

  uint16_t preambleLen = LORA_PREAMBLE_LENGTH;
  bool fixLen = LORA_FIXED_PAYLOAD;
  bool crcOn = LORA_CRC;

  // Frequency Hop
  // Enables disables the intra-packet frequency hopping
  bool freqHop = LORA_FREQ_HOP;
  // Number of symbols between each hop
  uint8_t hopPeriod = 0;

  bool iqInverted = false;
  uint32_t timeout = 3000;  // milliseconds
};


class EzRadio {

  public:

    RadioEvents_t* handler;
    EzRadioCfg cfg;

    EzRadio(uint8_t uuid, RadioEvents_t* handler, EzRadioCfg cfg);

    void reconfigure();

    void transmit(uint16_t id, uint8_t type, char* data);


    void ack(uint16_t id, uint8_t from, uint8_t sig);
    bool waitForAck(uint16_t id);

    void ping(uint16_t id, uint8_t client);

    bool read(RadioPacket* message);
    bool read(RadioPacket* message, uint16_t timeout);

  private:
    uint8_t uuid;
    void OnTxDone( void );
    void OnTxTimeout( void );

};




EzRadio* eZRadio_configure(uint8_t uuid, RadioEvents_t* events, EzRadioCfg cfg);

void eZRadioReceiveSignal(uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr );
ezradio.cpp
#include "ezradio.h"

static RadioPacket RECEIVED;

static bool eZRadioMessageReady = false;
static bool eZRadioReadingMessage = false;

static uint32_t eZRadio_lastTx = 0;
static uint32_t eZRadio_lastRx = 0;

const uint8_t SYM_5 = 0x7b;

const uint8_t HDR_SIZE = 8; // + sizeof(double) * 2;

void eZRadioReceiveSignal( uint8_t *payload, uint16_t size, int16_t rssi, int8_t snr ) {

  // message structure
  //
  // first two bytes = 18 bits of uint16_t message sequence id
  // third byte = 8 bits of client identifier
  //
  eZRadioMessageReady = false;

  if (size < HDR_SIZE || payload[5] != SYM_5 || payload[6] != 0x8c || payload[7] != ':') {
    // invalid message, throw it away
    for (int i = 0; i < 3; i++) {
      // flash the LED red on error reading messages
      turnOnRGB(0x550000, 120);
      turnOnRGB(0, 100);
    }
    return;
  }

  eZRadioReadingMessage = true;
  turnOnRGB(0x000500, 0);

  RECEIVED.at = millis();
  RECEIVED.id = (uint16_t)payload[0] | ((uint16_t)payload[1] << 8); // (uint16_t)((uint16_t*)payload)[0];
  RECEIVED.from = (short) payload[2] & 0x0F;
  RECEIVED.type = payload[3];
  RECEIVED.rssi = rssi;
  RECEIVED.snr = snr;
  RECEIVED.length = size - HDR_SIZE;

  RECEIVED.signalStrength = 100 + (min(-30, max(-130, rssi)) + 30);

  memcpy(RECEIVED.data, &payload[HDR_SIZE], RECEIVED.length);

  switch (payload[3]) {
    case '1': // ack

      break;
    case '2': // ping


    case 'P': // position
      char readBuffer[16];
      int i = HDR_SIZE;

      for (; i + 1 < size && payload[i] != ' '; i++) {
        readBuffer[i - HDR_SIZE] = payload[i];
      }
      readBuffer[i] = 0;

      RECEIVED.lat = String(readBuffer).toDouble();

      int k = i + 1;
      for (i = k; i + 1 < size && payload[i] != 0; i++) {
        readBuffer[i - k] = payload[i];
      }
      readBuffer[i] = 0;

      RECEIVED.lon = String(readBuffer).toDouble();
      break;
  }
  Serial.printf("receiving size=%d msg='%s'\n", size, &payload[HDR_SIZE]);


  eZRadio_lastRx = millis();
  eZRadioReadingMessage = false;
  eZRadioMessageReady = true;

  Radio.Rx(0);
  turnOnRGB(0, 0);
}


EzRadio::EzRadio(uint8_t uuid, RadioEvents_t* handler, EzRadioCfg cfg) {
  this->uuid = uuid;
  this->handler = handler;
  this->cfg = cfg;
  handler->RxDone = eZRadioReceiveSignal;
  Radio.Init(handler);
}

void EzRadio::reconfigure() {

  Radio.SetTxConfig( cfg.modem,
                     cfg.power,
                     0,
                     cfg.bandwidth,
                     cfg.datarate,
                     cfg.coderate,
                     cfg.preambleLen,
                     cfg.fixLen,
                     cfg.crcOn,
                     cfg.freqHop,
                     cfg.hopPeriod,
                     LORA_IQ_INVERSION, 3000
                   );

  Radio.SetRxConfig( cfg.modem,
                     cfg.bandwidth,
                     cfg.datarate,
                     cfg.coderate,
                     0, // bandwidthAfc
                     cfg.preambleLen,
                     LORA_SYMBOL_TIMEOUT,
                     cfg.fixLen,
                     64, // uint8_t payloadLen (if fixLen == true)
                     cfg.crcOn,
                     cfg.freqHop,
                     cfg.hopPeriod,
                     LORA_IQ_INVERSION,
                     true // contiunous
                   );

}

void EzRadio::OnTxDone( void ) {

}
void EzRadio::OnTxTimeout( void ) {

}


void EzRadio::ack(uint16_t id, uint8_t from, uint8_t sig) {
  byte channel = cfg.bandwidth;
  transmit(id, '1', "ACK");
}

void EzRadio::ping(uint16_t id, uint8_t client) {
  transmit(id, '2', "PING");
}

void EzRadio::transmit(uint16_t id, uint8_t type, char* data) {
  turnOnRGB(0x070000, 10);

  int size = strlen(data);
  char* txpacket = new char[size + HDR_SIZE];

  txpacket[0] = (uint8_t) (id & 0xFF);  // low bits
  txpacket[1] = (uint8_t) (id >> 8); // high bits


  txpacket[2] = uuid;
  txpacket[3] = type;
  txpacket[4] = 0x2a; // reserved
  txpacket[5] = SYM_5; // reserved
  txpacket[6] = 0x8c; // reserved
  txpacket[7] = ':';


  sprintf(&txpacket[HDR_SIZE], data, size);
  txpacket[size + HDR_SIZE] = 0;

  Serial.printf("\r\nsending packet \"%s\" , length %d\r\n", &txpacket[HDR_SIZE], HDR_SIZE + size);
  Radio.Send( (uint8_t *)txpacket, size + HDR_SIZE);
  eZRadio_lastTx = millis();

  turnOnRGB(0, 0);
  free(txpacket);
}

bool EzRadio::read(RadioPacket* message) {
  return read(message, 0);
}

bool EzRadio::waitForAck(uint16_t msgId) {
  Radio.Rx(100);
  RadioPacket ack;
  if (read(&ack, 100)) {
    return ack.type == '1' && msgId == ack.id;
  }
  return false;
}

bool EzRadio::read(RadioPacket* message, uint16_t timeout) {
  timeout += millis();
  while ((!eZRadioMessageReady || eZRadioReadingMessage) && timeout > millis()) {
    delay(20); // reading a message now
  }

  if (!eZRadioMessageReady) {
    return false;
  }

  message->at = RECEIVED.at;
  message->id = RECEIVED.id;
  message->from = RECEIVED.from;
  message->rssi = RECEIVED.rssi;
  message->snr = RECEIVED.snr;
  message->signalStrength = RECEIVED.signalStrength;
  message->length = RECEIVED.length;
  memcpy(message->data, RECEIVED.data, RECEIVED.length);
  message->lat = RECEIVED.lat;
  message->lon = RECEIVED.lon;
  message->age = millis() - RECEIVED.at;
  message->type = RECEIVED.type;

  eZRadioMessageReady = false;
  return true;
  ;
}


EzRadio* eZRadio_configure(uint8_t uuid, RadioEvents_t* events, EzRadioCfg cfg) {
  EzRadio* ez = new EzRadio(uuid, events, cfg);
  ez->reconfigure();
  return ez;
}

Joystick Example

sketchbook.ino

#include "thumbstick.h"

// set pin numbers for switch, joystick axes, and LED:
const int switchPin = 6;    // switch to turn on and off mouse control
const int xAxis = A0;       // joystick X axis pin
const int yAxis = A1;       // joystick Y axis pin

class MenuListener {
  public:
  bool press();
  void release(const int ms);
};

bool MenuListener::press() {
  return true;
}

void MenuListener::release(const int ms) {

  if (ms > 7000) {
    // system reset
    Serial.println("RESET");
  } else if (ms > 3000) {
    // extra
    Serial.println("Extra");
  } else {
    // normal click release, take some action.
    Serial.println("Select");
  }
}

// Singleton instances
MenuListener menuListener = MenuListener();
ThumbstickButton button = ThumbstickButton(6, xAxis, yAxis, (PressButton)&menuListener.press, (ReleaseButton)&menuListener.release);

void setup() {

  Serial.begin(115200);

}

void loop() {

  if (button.pushed()) {

  }
  
  ThumbstickPosition p = button.position();

  if (p.x != 0 || p.y != 0) {
    char jxy[30];
    sprintf(jxy, "%i,%i %i,%i\0", p.x, p.y, p.dx, p.dy);
    Serial.println(jxy);
  }
  
  delay(5);
  
}
thumbstick.h
#include "Arduino.h"

typedef bool (*PressButton) ();
typedef void (*ReleaseButton) (const int ms);

class ThumbstickPosition {
  public:
    int x = 0;
    int y = 0;
    int dx = 0;
    int dy = 0;
    
    ThumbstickPosition();
    ThumbstickPosition(const int x, const int y, const int dx, const int dy);

  private:
  
    bool motion = false;
};

class ThumbstickButton {
  public:

    ThumbstickButton(const int pin, const int xAxis, const int yAxis, PressButton btnPress, ReleaseButton btnRelease);
    bool pushed();
    unsigned long age();
    ThumbstickPosition position();
  
  private:
    int pin;
    int xAxis;
    int yAxis;
    
    unsigned long since = 0;
    PressButton btnPress;
    ReleaseButton btnRelease;
    ThumbstickPosition positions[2];
    
    bool pressed = false;
    int8_t peek = 0;
    int8_t current = 0;
    int8_t previous = 0;
    
    int8_t read();
    int readAxis(int axis);
};

thumbstick.cpp

#include "thumbstick.h"


// parameters for reading the joystick:
const int range = 128;    // output range of X or Y movement
const int threshold = 8;  // resting threshold

ThumbstickPosition::ThumbstickPosition() {
  
}

ThumbstickPosition::ThumbstickPosition(const int x, const int y, const int dx, const int dy) {
  this->x = x;
  this->y = y;
  this->dx = dx;
  this->dy = dy;
}

ThumbstickButton::ThumbstickButton(const int pin, const int xAxis, const int yAxis, PressButton btnPress, ReleaseButton btnRelease) {
  this->pin = pin; 
  this->xAxis = xAxis;
  this->yAxis = yAxis;
  this->btnPress = btnPress;
  this->btnRelease = btnRelease;
  pinMode(pin, INPUT_PULLUP);  // the switch pin
}

unsigned long ThumbstickButton::age() {
  return millis() - this->since;
}

int8_t ThumbstickButton::read() {
  int8_t r = digitalRead(this->pin);  // 1=up or 0=down.

  if (r == this->peek) {
    if (millis() - this->since > 40) {
      this->previous = this->current;
      this->current = r;
    }
  } else {
    // state changed
    
    this->peek = r;
    if (this->current == LOW && r == HIGH) {
      // button released
      const int ms = millis() - this->since;
      this->previous = this->current;
      this->current = r;
      this->pressed = false;
      this->btnRelease(ms);
    }
    
    this->since = millis();
  }
  
  return this->current;
}

bool ThumbstickButton::pushed() {
  if (this->read() == LOW) {
    uint32_t lastPress = millis();
    if (!this->pressed) {
      return this->pressed = this->btnPress();
    }
  }
  return false;
}

ThumbstickPosition ThumbstickButton::position() {
  this->positions[1] = this->positions[0];
  const int x = this->readAxis(xAxis);
  const int y = this->readAxis(yAxis);
  
  return this->positions[0] = ThumbstickPosition(x, y, x - this->positions[1].x, y - this->positions[1].y);
}

int ThumbstickButton::readAxis(int axis) {
  /*
  reads an axis (0 or 1 for x or y) and scales the analog input range to a range
  from 0 to <range>
 */

  // read the analog input:
  int reading = analogRead(axis);

  // map the reading from the analog input range to the output range:
  reading = map(reading, 0, 1023, 0, range);

  // if the output reading is outside from the rest position threshold, use it:
  int distance = reading - (range / 2);

  if (abs(distance) < threshold) {
    return 0;
  }

  // return the distance for this axis:
  return distance;

}

MPU 6050 Arduino Library

MPU 6050 Arduino Library

The MPU-6050 IMU (Inertial Measurement Unit) is a 3-axis accelerometer and 3-axis gyroscope sensor. The accelerometer measures the gravitational acceleration, and the gyroscope measures the rotational velocity. Additionally, this module also measures temperature.

Required Arduino IDE Library

gyro.h
#include "MPU6050.h"

class GyroPacket {

public:
  int16_t _ax, _ay, _az;
  int16_t _gx, _gy, _gz;
  int16_t _rx, _ry, _rz;

  double t,tx,tf,_pitch,_roll,_yaw;
  
  GyroPacket(int address, MPU6050 sensor);

  bool isMoving();

  // acceleration
  int16_t ax();
  int16_t ay();
  int16_t az();

  // gyro
  int16_t gx();
  int16_t gy();
  int16_t gz();

  // rotation
  int16_t rx();
  int16_t ry();
  int16_t rz();

  double pitch();
  double roll();
  double yaw();
  
private:
  int address;
  bool moving;

  //convert the accel data to pitch/roll
  void getAngle(int a, int b, int c);
  
};
gyro.cpp
#include <math.h>
#include <Wire.h>
#include "I2Cdev.h"
#include "MPU6050.h"
#include "gyro.h"


GyroPacket::GyroPacket(int address, MPU6050 sensor) {
  this->address = address;

  sensor.getMotion6(
    &_ax, &_ay, &_az,
    &_gx, &_gy, &_gz
  );

  sensor.getRotation(
    &_rx, &_ry, &_rz
  );

  getAngle(_ax, _ay, _az);
  
  this->t = ((sensor.getTemperature() - 1600) / 340) + 36.53;
  this->tf = (this->t * 9 / 5) + 32;

}

int16_t GyroPacket::ax() {
  return _ax;
}
int16_t GyroPacket::ay() {
  return _ay;
}
int16_t GyroPacket::az() {
  return _az;
}
int16_t GyroPacket::gx() {
  return _gx;
}
int16_t GyroPacket::gy() {
  return _gy;
}
int16_t GyroPacket::gz() {
  return _gz;
}
int16_t GyroPacket::rx() {
  return _rx;
}
int16_t GyroPacket::ry() {
  return _ry;
}
int16_t GyroPacket::rz() {
  return _rz;
}

//
////convert the accel data to pitch/roll
void GyroPacket::getAngle(int a, int b, int c) {
  double x = (double)a;
  double y = (double)b;
  double z = (double)c;
  double pitch = atan(x / sqrt((y * y) + (z * z)));
  double roll = atan(y / sqrt((x * x) + (z * z)));
  double yaw = atan(z / sqrt((x * x) + (y * y)));
  //convert radians into degrees
  this->_pitch = pitch * (180.0 / M_PI); //3.14159265359);
  this->_roll = roll * (180.0 / M_PI); //3.14159265359) ;
  this->_yaw = yaw * (180.0 / M_PI); //3.14159265359) ;

}

//convert the accel data to pitch/roll
double GyroPacket::pitch() {
  return _pitch;
// double pitch = atan((double)_ax / sqrt(((double)_ay * _ay) + (_az, _az)));
// return pitch * (180.0/M_PI);
}

double GyroPacket::roll() {
  return _roll;
// double roll = atan((double)_ay / sqrt(((double)_ax * _ax) + (_az * _az)));
// return roll * (180.0/M_PI);
}

double GyroPacket::yaw() {
  return _yaw;
// double yaw = atan((double)_az / sqrt(((double)_ax * _ax) + (_ay * _ay)));
// return yaw * (180.0/M_PI);
}

T-LOC Build

2023 April – North Site

NOGO FOR LAUNCH – Dec 4, 2021

Members and Friends,
There is a scheduled Northern Colorado Rocketry launch event on Sat, 12/4 at the Atlas launch site.

Members and Friends,

Go figure. Opportunities change that quickly…..

National Forests along northern Front Range enter fire restrictions; provide fall safety reminders

Release Date: Nov 30, 2021

FORT COLLINS, Colo. (Nov. 30, 2021) – Due to dry and warm conditions and recent fire starts, the Arapaho and Roosevelt National Forests’ Clear Creek, Boulder and Canyon Lakes ranger districts along with the Pawnee National Grassland are enacting Stage 1 fire restrictions effective at 12:01 a.m. Wednesday, Dec. 1. Stage 1 fire restrictions limit where and what type of fires visitors can have and remain in place until rescinded.

The following is PROHIBITED under Stage 1 Fire Restrictions:

  • Igniting, building, maintaining, attending, or using a fire (including fires fueled by charcoal or briquettes) outside of a permanent metal or concrete fire pit or grate that the Forest Service has installed and maintained at its developed recreation sites (campgrounds and picnic areas).
  • Smoking, except in an enclosed vehicle or building, a developed recreation site, or while stopped in an area at least three feet in diameter that is barren or cleared of all flammable materials.
  • Operating a chainsaw without an effective and properly installed spark arrestor, a fire extinguisher kept with the operator, and a shovel.
  • Blasting, welding, or operating a torch with an open flame without being in a cleared area of at least 10 feet in diameter and having a fire extinguisher kept with the operator.
  • Using an explosive. This includes but is not limited to fuses, blasting caps, fireworks, rockets, exploding targets, tracers, and incendiary ammunition. (Fireworks are always prohibited on National Forest lands).
  •  

Violation of Stage 1 fire restrictions could result in a maximum fine of $5,000 for an individual or $10,000 for an organization, or imprisonment for more than six months, or both. If responsible for causing a wildfire, one could be held accountable for suppression costs of that fire.

Looks like SCORE is still planning to launch at the Hudson Ranch.

Northern Colorado Rocketry – Affiliated with both NAR and TRA

Electronics

Rocketry has experienced a revolution in technology now available to the common hobbyist.

There are a variety of vendors that now provide electronic payload packages to record flight characteristics, altitude, speed, acceleration, and more.

Altimeters can (and usually do) include capabilities to control apogee and main parachute deployment events with black powder charges.

In addition, GPS and Radio tracking systems are also available.

RRC3

Missile Works RRC3

Timer

Easy Mini

2021 MHM Overnight Build Contest

This contest will be held during the Mile High Mayhem (June 4, 5 & 6) Weekend Launch ONLY.

Objective

Build and fly a model rocket at the 2021 Mile High Mayhem weekend launch; fly it, and successfully recover.

Contestants will need to buy or bring their own rocket kit and any necessary building supplies.

Rules

Construction can begin after 4:00PM on Friday June 4.
Record a successful flight before 10:00AM Sunday June 6.
Declare your flight as a contest flight prior to launch.

* Open to any NCR club member.
* No build restrictions, bring any additional parts and supplies.
* Flights must be witnessed by another club member.

Entry Information

A registration sheet will be available at the LCO desk; contact Brad Morse if the sheet is missing.

Declare your flight as “Overnight Build” contest on your flight card.

Remember to record your flight’s success or failure.

Build Constraints

The rocket must be stable and constructed with materials approved by NAR or Tripoli, you may also use a kit.

Tie Breakers

  • Highest motor impulse

Prizes

The prize is bragging rights.

2021 NCR May Days [North Site]

Launch Summary

Happy May Day!  Huge THANKS to Jack Matthews for towing both ways and for range set up with the help of Scott Dukes, David Pinter and everyone else that assisted.  We were OK to proceed as planned with the scheduled Saturday May 1st and Sunday May 2nd launch event, at the North site, subject to the normal precautions.  Basic COVID hygiene protocols were still in effect:  If you had a fever or if you felt  sick, don’t show up;  keep at least 6′ or more between your prep area and the next person.

An orientation briefing for participants was held and included a discussion of safety and emergency procedures, fire conditions and instruction, applicable rules and regulations, and what is expected of participants with respect to the use of NFS lands under our special use permit. Waiver was from 9 AM to 6 PM but we were pretty much done by 5 when weather started to threaten.  

* Photos uploaded by George Barnes, huge thanks!

Launch Logs

 

We are Good to Go with our scheduled and approved launch on Saturday April 5th and Sunday April 6th, from the North Site, subject to change – Mostly cloudy and High Winds predicted.  Range and waiver should be active by 9AM on Saturday.   The Pawnee National Grassland remains fragile and dry, so extra precautions are in order.  Please stay on the authorized roads and please don’t park more than 100’ off the road at the flight line.

 

 

 

Vehicles must remain on approved/maintained roadways.  Driving on the Pawnee Grasslands is not allowed during any NCR sanctioned activity.